How to Efficiently Count 100 Million Redis Keys Without Crashing Your Cluster
This article explains why the KEYS command is unsafe for large Redis keyspaces, introduces the SCAN command and multithreaded scanning techniques, compares performance, and offers alternative counting strategies to reliably count hundreds of millions of keys.
Introduction
Many engineers accidentally run
KEYS *on a Redis cluster with millions of keys, causing the server to block and services to fail.
Why not use KEYS?
Redis is single‑threaded;
KEYS *scans the entire keyspace (O(N)). While scanning, no other commands are processed. For 100 million keys, even a 0.1 µs lookup per key takes about 10 seconds, leading to memory blow‑up and possible OOM.
Fatal triple hit :
Time complexity: 100 M keys ≈ 10 s.
Memory storm: returning too many keys can exhaust client memory.
Cluster limitation:
KEYSonly sees keys on the local node.
SCAN command
The
SCANcommand iterates with a cursor, returning a small batch of keys each call, avoiding blocking.
Basic Java SCAN implementation
public long safeCount(Jedis jedis) {
long total = 0;
String cursor = "0";
ScanParams params = new ScanParams().count(500);
do {
ScanResult<String> rs = jedis.scan(cursor, params);
cursor = rs.getCursor();
total += rs.getResult().size();
} while (!"0".equals(cursor));
return total;
}Scanning 500 keys per batch means ~200 000 calls for 100 M keys, roughly 10 minutes.
Multithreaded concurrent SCAN
Using a thread pool and atomic counters to parallelize the scan reduces time dramatically.
public long parallelCount(JedisPool pool, int threads) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(threads);
AtomicLong total = new AtomicLong(0);
List<String> cursors = new ArrayList<>();
for (int i = 0; i < threads; i++) {
cursors.add(String.valueOf(i));
}
CountDownLatch latch = new CountDownLatch(threads);
for (String cursor : cursors) {
executor.execute(() -> {
try (Jedis jedis = pool.getResource()) {
String cur = cursor;
do {
ScanResult<String> rs = jedis.scan(cur, new ScanParams().count(500));
cur = rs.getCursor();
total.addAndGet(rs.getResult().size());
} while (!"0".equals(cur));
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
return total.get();
}On a 32‑core CPU, this approach finishes in about 18 seconds (CPU usage ≈ 800%).
Distributed cluster parallel count
When using Redis Cluster, each master node scans its own slots in parallel using
parallelStream.
public long clusterCount(JedisCluster cluster) {
Map<String, JedisPool> nodes = cluster.getClusterNodes();
AtomicLong total = new AtomicLong(0);
nodes.values().parallelStream().forEach(pool -> {
try (Jedis jedis = pool.getResource()) {
if (jedis.info("replication").contains("role:slave")) return;
String cursor = "0";
do {
ScanResult<String> rs = jedis.scan(cursor, new ScanParams().count(500));
total.addAndGet(rs.getResult().size());
cursor = rs.getCursor();
} while (!"0".equals(cursor));
}
});
return total.get();
}Alternative solutions
Built‑in counter :
INFO keyspacegives an approximate count instantly (O(1)) but includes expired keys.
Real‑time incremental counting : Use keyspace notifications to maintain an exact counter, at the cost of extra memory, CPU and network traffic.
Cost and hardware guidelines
CPU‑bound: threads = cores × 1.5
IO‑bound: threads = cores × 3
Control batch size (COUNT) to stay within memory limits.
When to choose which method
Exact statistics → distributed parallel SCAN; real‑time queries → incremental counter; trend analysis → sampling; avoid full
KEYSscans.
Understanding data characteristics is more important than brute‑force effort in the era of massive datasets.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.