Redis 如何高效安全的遍历所有 Key?

#中间件#面试八股

Redis 如何高效安全的遍历所有 Key?

结论:不要用 KEYS *。正确做法是 SCAN——基于游标 + 位反转增量遍历的无状态迭代器,每次只返回一小批,不阻塞主线程。它提供的是弱保证:从遍历开始到结束一直存在的 key 至少返回一次,但可能返回重复、也可能漏掉遍历期间新增或删除的 key。生产上配合 MATCH、COUNT、去重与限流使用。

一、KEYS、SCAN 与业务侧遍历

方式复杂度阻塞一致性结论
KEYS patternO(N)全库扫描期间完全阻塞快照式,准确只允许在离线环境用
SCAN cursor MATCH p COUNT n每次 O(1) 摊还每次最多几十微秒弱一致,可能重复生产唯一推荐
HSCAN / SSCAN / ZSCAN同上同上同上遍历大集合内部元素
从库上执行 SCAN同上不影响主库有复制延迟大批量巡检的推荐姿势

二、游标为什么是"位反转"的

哈希表扩容/缩容时,桶的编号会变:mask 从 3(2^2-1) 变成 7(2^3-1)
若按 0,1,2,3 顺序遍历,扩容后已扫过的桶可能被重复或跳过。
Redis 的做法:把游标按位反转后再递增(reverse binary iteration)

游标序列(mask=7,位反转后):
  0 → 4 → 2 → 6 → 1 → 5 → 3 → 7
好处:无论表在这期间扩容还是缩容,已遍历的桶不会漏,最多重复。

因此 SCAN 的游标不是"第几页",不能做分页:你不能用 SCAN 100 COUNT 10 取第 11 页,每次都必须从上一次返回的游标继续,直到返回 0 表示结束。

三、正确用法

# 1) 基础迭代:COUNT 是提示值,实际返回条数可多可少
redis-cli SCAN 0 MATCH 'order:*' COUNT 1000

# 2) 循环直到游标为 0(脚本示例)
cursor=0
while :; do
  out=$(redis-cli SCAN $cursor MATCH 'order:*' COUNT 1000)
  cursor=$(echo "$out" | head -1)
  echo "$out" | tail -n +2
  [ "$cursor" = "0" ] && break
done

# 3) 只关心类型(6.0+),减少客户端过滤
redis-cli SCAN 0 TYPE hash COUNT 500

# 4) 遍历大集合的内部元素,而不是整个库
redis-cli HSCAN user:1001 0 COUNT 100
redis-cli ZSCAN rank:week 0 COUNT 100
// 生产封装:限速 + 去重 + 可中断
public void scanAll(String pattern, Consumer<String> handler) {
    ScanOptions opts = ScanOptions.scanOptions().match(pattern).count(500).build();
    Set<String> seen = new HashSet<>();          // 应对重复返回
    try (Cursor<String> cursor = redis.scan(opts)) {
        while (cursor.hasNext()) {
            String key = cursor.next();
            if (seen.add(key)) { handler.accept(key); }
            RateLimiter.acquire();               // 每秒上限,避免打满
        }
    }
}

四、弱一致性的边界

  • 会重复:rehash 过程中同一个 key 可能出现在两个桶里,被返回两次,必须去重(幂等处理)。
  • 会漏新:遍历开始后才创建的 key 不保证返回;遍历期间被删除的 key 也可能仍被返回。
  • 不要当快照:需要"某一时刻的完整 key 列表"就 BGSAVE 后离线分析 RDB,或用 rdb-tools/redis-rdb-cli。
  • COUNT 不是上限:它只是"每次扫描的桶数提示",返回条数可能远大于 COUNT(桶内链表长时)。

五、常见追问

  • 问:SCAN 会不会阻塞主线程?答:每次调用只扫有限个桶,耗时通常在几十微秒级;但如果 COUNT 设得极大、或遇到大 key 的 HSCAN,单次仍可能变慢。
  • 问:为什么不能用 KEYS 做巡检?答:O(N) 全库遍历且期间不处理其他命令,百万级 key 会造成秒级卡顿,等同于故障。
  • 问:SCAN 0 每次都能从头开始吗?答:能,游标 0 表示"从头开始"。但注意返回 0 也表示"遍历结束",二者靠上下文区分。
  • 问:如何统计某前缀的 key 数量?答:遍历时累加(接受弱一致),或维护计数 key(写时 INCR);DBSIZE 只能给出总数。
  • 问:集群模式下怎么扫全库?答:对每个主节点分别 SCAN,客户端按节点建立连接;--cluster call 或 cluster 客户端的 getPartitions() 都能拿到节点列表。
  • 问:大 key 巡检怎么做?答:redis-cli --bigkeys 内部就是 SCAN + TYPE + 抽样 MEMORY USAGE;更精细用 --memkeys 或离线分析 RDB。

小结

  • 遍历三原则:不用 KEYS、用 SCAN 的游标迭代、结果去重并限速。
  • SCAN 的保证只有"一直存在的 key 至少出现一次",其余(完整性、顺序、快照)都不要假设。
  • 只要把 SCAN 当成"最终能扫完、但会重复"的工具,巡检脚本就不会写错。
笔记加载中…