SCAN 与 KEYS 的区别:为什么说 SCAN 是 KEYS 的”安全替身”

SCAN 与 KEYS 的区别:为什么说 SCAN 是 KEYS 的”安全替身”

核心区别一览

特性 KEYS SCAN
时间复杂度 O(N) 全量扫描 O(1) per 返回
是否阻塞 阻塞 Redis 主线程 不阻塞(分批次返回)
返回结果 一次性返回所有匹配 key 分批次返回,每批 COUNT 个
数据量影响 数据量越大越慢 几乎不受数据量影响
一致性 返回命令执行时的快照 可能包含增删的 key
游标支持 不支持 支持游标分页
额外参数 仅支持 pattern 支持 MATCH/COUNT/TYPE

深入理解 SCAN 的工作原理

SCAN 命令语法

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
  • cursor:游标,从 0 开始,返回结果中的 cursor 为 0 表示遍历完毕
  • MATCH:key 匹配模式(可选)
  • COUNT:每批返回的预估 key 数量(可选,默认 10)
  • TYPE:key 类型过滤(可选,如 string、list、set)

示例

127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 100
1) "2321"           -- 下一次遍历的游标
2) 1) "user:1001"   -- 匹配到的 key
   2) "user:1002"
   3) "user:1005"
   ...

127.0.0.1:6379> SCAN 2321 MATCH user:* COUNT 100
1) "0"              -- 游标为 0表示遍历完成
2) 1) "user:2001"
   2) "user:2005"
   ...

为什么 SCAN 不会阻塞

KEYS 的工作方式:

Redis 接到 KEYS * → 遍历整个哈希表 → 一次性返回所有 key → 期间无法处理其他命令

SCAN 的工作方式:

Redis 接到 SCAN 0 → 扫描哈希表的一个"桶" → 返回该桶中的 key → 处理其他命令
Redis 接到 SCAN cursor → 继续扫描下一个"桶" → ...

SCAN 利用 Redis 哈希表的桶结构,每次只访问有限数量的桶,大大缩短了单次阻塞时间。

SCAN 的潜在问题

1. 返回结果可能重复

SCAN 在遍历过程中,哈希表可能发生 rehash 或 key 被修改,导致某些 key 被重复返回。

# 安全做法:使用 set 去重
seen = set()
cursor = 0
while cursor:
    cursor, keys = r.scan(cursor, match="pattern:*", count=100)
    for key in keys:
        if key not in seen:
            seen.add(key)
            process(key)

2. COUNT 参数是”建议值”而非精确值

COUNT 参数告诉 Redis 应该返回多少元素,但 Redis 可能返回多于或少于 COUNT 的数量。它只是对扫描步长的提示。

3. MATCH 模式的过滤时机

MATCH 是在 SCAN 已经返回 key 之后才进行匹配过滤。如果匹配率很低,可能需要更多轮次才能收集到足够的结果。

-- 假设有 1000 万 key,但匹配 user:* 的只有 100 个
-- COUNT 设为 100,需要扫描很多轮才能找到 100 个匹配项
-- 这不会阻塞 Redis,但循环次数会增多

SCAN 系列命令

Redis 为每种数据类型提供了对应的 SCAN 变体:

命令 目标数据类型 时间复杂度
SCAN 所有 key O(1) per call
SSCAN Set O(1) per call
HSCAN Hash O(1) per call
ZSCAN Sorted Set O(1) per call
-- 安全遍历大哈希
HSCAN user:profile 0 MATCH field:* COUNT 50

-- 安全遍历大集合
SSCAN big_set 0 COUNT 100

-- 安全遍历大有序集合
ZSCAN big_zset 0 COUNT 50

什么时候可以用 KEYS

虽然 KEYS 在线上环境危险,但在以下有限场景中可以使用:

  • 开发环境验证数据是否存在
  • 测试环境数据量小(< 10,000 个 key)时调试
  • 单机低负载时手动排查问题
-- 安全的 KEYS 调用:先确认 key 数量少
DBSIZE  -- 查看 key 总数
KEYS *  -- 仅在 key 数很少时使用

性能对比

# 测试:100 万 key 的 Redis
# KEYS *
start = time.time()
keys = r.keys('*')
print(f"KEYS *: {time.time() - start:.2f}s")  
# 输出: KEYS *: 0.45s

# SCAN
start = time.time()
keys = []
cursor = 0
while True:
    cursor, batch = r.scan(cursor, count=1000)
    keys.extend(batch)
    if cursor == 0:
        break
print(f"SCAN: {time.time() - start:.2f}s")
# 输出: SCAN: 0.52s (但不会阻塞其他请求)

虽然总时间相近,但 SCAN 分散了阻塞时间,其他请求可以”见缝插针”地执行。

面试要点

  • KEYS 阻塞是因为全量一次返回,SCAN 不阻塞是因为分批次返回
  • SCAN 可能重复返回同一条 key,需要客户端去重
  • SCAN 不会返回遍历期间新增或删除的 key(弱一致性)
  • COUNT 是期望值而非精确值
  • 不要忘记游标为 0 表示遍历结束
© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容