Redis SCAN vs KEYS:生产环境安全遍历 Key 的正确姿势
KEYS * 是你能在生产环境 Redis 上运行的最危险的命令。它在扫描整个键空间时阻塞服务器。本指南展示安全的替代方案:SCAN 系列命令。
问题场景
你需要找到所有匹配某个模式的 Key。也许你在清理过期缓存、迁移数据或调试。最直觉的做法:
KEYS cache:user:*
100 个 Key 时没问题。1000 万个 Key 时,它会阻塞 Redis 数秒——其他命令都无法执行。你的整个应用冻结。
为什么 KEYS 很危险
KEYS 的时间复杂度是 O(N),N 是数据库中 Key 的总数。它同步运行,阻塞单线程的 Redis 事件循环。
SET key:1 "a"
SET key:2 "b"
SET key:3 "c"
KEYS key:*
看起来无害。但在有数百万 Key 的生产环境中,KEYS * 可能:
- 阻塞 Redis 1–10+ 秒。
- 导致所有连接的客户端超时。
- 在你的应用中触发级联故障。
- 触发熔断器和健康检查失败。
Redis 官方文档明确说:"不要在生产环境使用 KEYS。"
SCAN:安全的替代方案
SCAN 使用游标增量遍历键空间。每次调用返回一小批 Key,永远不会长时间阻塞。
基本用法
SET user:1001 "Alice"
SET user:1002 "Bob"
SET user:1003 "Charlie"
SET product:2001 "Widget"
SET product:2002 "Gadget"
SET cache:temp:1 "data1"
SET cache:temp:2 "data2"
SCAN 0 COUNT 3
SCAN 返回两样东西:
- 一个游标(用于下一次 SCAN 调用)。
- 一批 Key。
当游标返回 0 时,遍历完成。
完整遍历
SCAN 0 COUNT 3
取结果中的游标用于下一次调用:
SCAN <上次返回的游标> COUNT 3
重复直到游标返回 0。
模式匹配
SCAN 0 MATCH user:* COUNT 10
只返回匹配 user:* 模式的 Key。但注意:SCAN 在获取批次之后才应用模式过滤,所以有些调用可能返回零结果,即使存在匹配的 Key。
TYPE 过滤(Redis 6.0+)
SCAN 0 TYPE string COUNT 10
SCAN 0 TYPE hash COUNT 10
SCAN 0 TYPE zset COUNT 10
按数据类型过滤。当你只想找所有 Hash 时很有用。
HSCAN:遍历 Hash 字段
对于大 Hash,HGETALL 相当于 KEYS *——会阻塞。改用 HSCAN:
HSET user:profile name "Alice" age "30" email "alice@example.com" role "admin" city "Springfield" country "US"
HSCAN user:profile 0 COUNT 2
带模式匹配
HSCAN user:profile 0 MATCH *a* COUNT 10
只返回匹配模式的字段。
SSCAN:遍历 Set 成员
SADD tags "redis" "database" "cache" "nosql" "performance" "tutorial" "guide"
SSCAN tags 0 COUNT 3
SSCAN tags 0 MATCH *re* COUNT 10
ZSCAN:遍历有序集合成员
ZADD leaderboard 100 "alice" 200 "bob" 150 "charlie" 300 "diana" 250 "eve"
ZSCAN leaderboard 0 COUNT 2
ZSCAN leaderboard 0 MATCH *a* COUNT 10
返回成员及其分数。
COUNT 参数
COUNT 是一个提示,不是保证。它告诉 Redis 每次迭代大约扫描多少元素。
SET a "1"
SET b "2"
SET c "3"
SET d "4"
SET e "5"
SCAN 0 COUNT 2
Redis 可能返回多于或少于 COUNT 个元素。默认值是 10。
如何选择 COUNT
- 小数据库(< 1 万 Key):
COUNT 100就够了。 - 中等数据库(1 万–100 万 Key):
COUNT 1000平衡速度和响应性。 - 大数据库(> 100 万 Key):
COUNT 5000–10000加快遍历,但要监控延迟。
实用模式
按模式删除 Key
安全删除所有匹配模式的 Key:
SET cache:temp:1 "data1"
SET cache:temp:2 "data2"
SET cache:temp:3 "data3"
SET cache:temp:4 "data4"
SCAN 0 MATCH cache:temp:* COUNT 100
在应用中循环:
SCAN cursor MATCH pattern COUNT 100DEL(或UNLINK)返回的 Key。- 重复直到游标为 0。
DEL cache:temp:1 cache:temp:2
SCAN 0 MATCH cache:temp:* COUNT 100
DEL cache:temp:3 cache:temp:4
按模式统计 Key 数量
SCAN 0 MATCH user:* COUNT 100
在所有迭代中累加计数。没有内置的"统计匹配 Key 数量"命令。
查找没有 TTL 的 Key
遍历并检查每个 Key:
SET permanent:key "no ttl"
SET temp:key "has ttl" EX 300
TTL permanent:key
TTL temp:key
在脚本中:SCAN,然后对每个 Key 检查 TTL。如果 TTL 返回 -1,该 Key 没有过期时间。
在数据库间迁移 Key
SCAN 0 MATCH migrate:* COUNT 100
对每个 Key,用 DUMP + RESTORE 迁移到目标数据库或实例。
保证与限制
SCAN 保证什么
- 从遍历开始到结束一直存在的 Key 一定会被返回。
- 遍历最终会完成(游标返回 0)。
SCAN 不保证什么
- 不保证无重复:一个 Key 可能被返回多次。你的应用必须处理去重。
- 不保证一致性:遍历期间添加或删除的 Key 可能出现也可能不出现。
- 不保证精确 COUNT:每次调用返回的元素数量不固定。
处理重复
SET key:1 "a"
SET key:2 "b"
SET key:3 "c"
SCAN 0 COUNT 2
在应用中用 Set 追踪已见过的 Key:
# 伪代码
seen = set()
cursor = 0
while True:
cursor, keys = redis.scan(cursor, count=100)
for key in keys:
if key not in seen:
seen.add(key)
process(key)
if cursor == 0:
break
性能对比
| 命令 | 时间复杂度 | 是否阻塞 | 生产环境安全 |
|---|---|---|---|
| KEYS * | O(N) | 是(全量扫描) | 否 |
| SCAN | 每次调用 O(1) | 否(增量) | 是 |
| HGETALL | O(N) | 是(全量 Hash) | 大 Hash 有风险 |
| HSCAN | 每次调用 O(1) | 否(增量) | 是 |
常见坑点
- 忘记处理游标 0:只有当游标返回
0时遍历才完成。不要在固定次数调用后停止。 - 忽略重复:SCAN 可能返回同一个 Key 两次。始终在应用中去重。
- 在脚本中使用 KEYS:即使在 Lua 脚本中,
KEYS也会阻塞。在应用代码中改用SCAN。 - COUNT 太小:
COUNT 1在大数据库上遍历极慢。至少用COUNT 100。 - 模式匹配开销:复杂的 glob 模式(如
*foo*bar*)更慢。保持模式简单。 - MATCH 返回空批次:SCAN 可能返回空数组,即使存在匹配的 Key。这是正常的——继续遍历直到游标为 0。
SET user:1 "a"
SET product:1 "b"
SET product:2 "c"
SET product:3 "d"
SCAN 0 MATCH user:* COUNT 2
如果采样的批次只包含 product:* Key,你可能得到空结果。下一次 SCAN 调用可能找到 user:1。
什么时候 KEYS 其实没问题
- 在本地实例上开发/调试。
- Key 数量 < 1000 的 Redis 实例。
- 在副本(不是主节点)上运行一次性脚本。
- 在
redis-cli中快速检查小数据库。
SET test:1 "a"
SET test:2 "b"
SET test:3 "c"
KEYS test:*
DBSIZE
在线编辑器试一试
前往 Redis 在线编辑器 练习安全遍历:
SET user:1001 "Alice"
SET user:1002 "Bob"
SET user:1003 "Charlie"
SET product:2001 "Widget"
SET product:2002 "Gadget"
SET cache:temp:1 "data"
SCAN 0 MATCH user:* COUNT 10
SCAN 0 MATCH product:* COUNT 10
SCAN 0 COUNT 3
HSET profile name "Alice" age "30" email "alice@example.com"
HSCAN profile 0 COUNT 2
ZADD scores 100 "alice" 200 "bob" 150 "charlie"
ZSCAN scores 0 COUNT 2
对比 KEYS * 和 SCAN 0 COUNT 10——两者都返回所有 Key,但 SCAN 安全地分批完成。这就是平稳的生产系统和凌晨 3 点事故之间的区别。