給你1億的Redis key,如何高效統計?
前言
有些小伙伴在工作中,可能遇到過這樣的場景:老板突然要求統計Redis中所有key的數量,你隨手執行了KEYS *
命令,下一秒監控告警瘋狂閃爍——整個Redis集群徹底卡死,線上服務大面積癱瘓。
今天這篇文章就跟大家一起聊聊如果給你1億個Redis key,如何高效統計這個話題,希望對你會有所幫助。
1.為什么不建議使用KEYS命令?
Redis的單線程模型是其高性能的核心,但也是最大的軟肋。
當Redis執行 KEYS *
命令時,內部的流程如下:
圖片
Redis的單線程模型是其高性能的核心,但同時也帶來一個關鍵限制:所有命令都是串行執行的。
當我們執行 KEYS * 命令時:
Redis必須遍歷整個key空間(時間復雜度O(N))
在遍歷完成前,無法處理其他任何命令
對于1億個key,即使每個key查找只需0.1微秒,總耗時也高達10秒!
致命三連擊:
- 時間復雜度:1億key需要10秒+(實測單核CPU 0.1μs/key)
- 內存風暴:返回結果太多可能撐爆客戶端內存
- 集群失效:在Cluster模式中只能查當前節點的數據。
如果Redis一次性返回的數據太多,可能會有OOM問題:
127.0.0.1:6379> KEYS *
(卡死10秒...)
(error) OOM command not allowed when used memory > 'maxmemory'
超過了最大內存。
那么,Redis中有1億key,我們要如何統計數據呢?
2.SCAN命令
SCAN
命令通過游標分批遍歷,每次只返回少量key,避免阻塞。
Java版基礎SCAN的代碼如下:
public long safeCount(Jedis jedis) {
long total = 0;
String cursor = "0";
ScanParams params = new ScanParams().count(500); // 每批500個
do {
ScanResult<String> rs = jedis.scan(cursor, params);
cursor = rs.getCursor();
total += rs.getResult().size();
} while (!"0".equals(cursor)); // 游標0表示結束
return total;
}
使用游標查詢Redis中的數據,一次掃描500條數據。
但問題來了:1億key需要多久?
- 每次SCAN耗時≈3ms
- 每次返回500key
- 總次數=1億/500=20萬次
- 總耗時≈20萬×3ms=600秒=10分鐘!
3.多線程并發SCAN方案
現代服務器都是多核CPU,單線程掃描是資源浪費。
看多線程優化方案如下:
圖片
多線程并發SCAN代碼如下:
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();
}
使用線程池、AtomicLong和CountDownLatch配合使用,實現了多線程掃描數據,最終將結果合并。
性能對比(32核CPU/1億key):
方案 | 線程數 | 耗時 | 資源占用 |
單線程SCAN | 1 | 580s | CPU 5% |
多線程SCAN | 32 | 18s | CPU 800% |
4.分布式環境的分治策略
如果你的系統重使用了Redis Cluster集群模式,該模式會將數據分散在16384個槽(slot)中,統計就需要節點協同。
流程圖如下:
圖片
每一個Redis Cluster集群中的master服務節點,都負責統計一定范圍的槽(slot)中的數據,最后將數據聚合起來返回。
集群版并行統計代碼如下:
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();
}
這里使用了parallelStream,會并發統計Redis不同的master節點中的數據。
5.毫秒統計方案
方案1:使用內置計數器
如果只想統計一個數量,可以使用Redis內置計數器,瞬時但非精確。
127.0.0.1:6379> info keyspace
# Keyspace
db0:keys=100000000,expires=20000,avg_ttl=3600
優點:毫秒級返回。
缺點:包含已過期未刪除的key,法按模式過濾數據。
方案2:實時增量統計
實時增量統計方案精準但復雜。
基于鍵空間通知的實時計數器,具體代碼如下:
@Configuration
publicclass KeyCounterConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory factory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener((message, pattern) -> {
String event = new String(message.getBody());
if(event.startsWith("__keyevent@0__:set")) {
redisTemplate.opsForValue().increment("total_keys", 1);
} elseif(event.startsWith("__keyevent@0__:del")) {
redisTemplate.opsForValue().decrement("total_keys", 1);
}
}, new PatternTopic("__keyevent@*"));
return container;
}
}
使用監聽器統計數量。
成本分析:
- 內存開銷:額外存儲計數器
- CPU開銷:增加5%-10%處理通知
- 網絡開銷:集群模式下需跨節點同步
6.如何選擇方案?
本文中列舉出了多個統計Redis中key的方案,那么我們在實際工作中如何選擇呢?
下面用一張圖給大家列舉了選擇路線:
圖片
各方案的時間和空間復雜度如下:
方案 | 時間復雜度 | 空間復雜度 | 精度 |
KEYS命令 | O(n) | O(n) | 精確 |
SCAN遍歷 | O(n) | O(1) | 精確 |
內置計數器 | O(1) | O(1) | 不精確 |
增量統計 | O(1) | O(1) | 精確 |
硬件法則:
- CPU密集型:多線程數=CPU核心數×1.5
- IO密集型:線程數=CPU核心數×3
- 內存限制:控制批次大小(count參數)
常見的業務場景:
- 電商實時大屏:增量計數器+RedisTimeSeries
- 離線數據分析:SCAN導出到Spark
- 安全審計:多節點并行SCAN
終極箴言:? 精確統計用分治? 實時查詢用增量? 趨勢分析用采樣? 暴力遍歷是自殺
真正的高手不是能解決難題的人,而是能預見并規避難題的人。
在海量數據時代,選擇比努力更重要——理解數據本質,才能駕馭數據洪流。