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


暂无评论内容