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 返回两样东西:

  1. 一个游标(用于下一次 SCAN 调用)。
  2. 一批 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

在应用中循环:

  1. SCAN cursor MATCH pattern COUNT 100
  2. DEL(或 UNLINK)返回的 Key。
  3. 重复直到游标为 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)否(增量)
HGETALLO(N)是(全量 Hash)大 Hash 有风险
HSCAN每次调用 O(1)否(增量)

常见坑点

  1. 忘记处理游标 0:只有当游标返回 0 时遍历才完成。不要在固定次数调用后停止。
  2. 忽略重复:SCAN 可能返回同一个 Key 两次。始终在应用中去重。
  3. 在脚本中使用 KEYS:即使在 Lua 脚本中,KEYS 也会阻塞。在应用代码中改用 SCAN
  4. COUNT 太小COUNT 1 在大数据库上遍历极慢。至少用 COUNT 100
  5. 模式匹配开销:复杂的 glob 模式(如 *foo*bar*)更慢。保持模式简单。
  6. 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 点事故之间的区别。