Redis SCAN 命令(完整指南)

Redis SCAN 命令:高效遍历大数据集的利器

在 Redis 的日常使用中,我们常常需要遍历所有的键(key)来完成某些操作,比如清理过期数据、迁移数据、统计信息等。然而,当数据量达到百万甚至千万级别时,传统的 KEYS * 命令就会成为性能杀手——它会阻塞整个 Redis 服务器,直到遍历完成。这时,Redis SCAN 命令 就成了我们应对大数据遍历的首选工具。

Redis SCAN 命令 是一种基于游标的渐进式遍历机制,它允许我们在不阻塞服务的前提下,逐步获取所有匹配的键。相比 KEYS *,它的核心优势在于“非阻塞”和“可中断”,特别适合生产环境中的高并发场景。


为什么不能用 KEYS *?

我们先来理解一下 KEYS * 为什么危险。想象一下,你的 Redis 里有 100 万个键,当你执行 KEYS * 时,Redis 需要一次性扫描整个键空间,把所有键名都加载进内存并返回。这个过程会占用大量 CPU 和内存资源,导致其他客户端请求延迟甚至超时。

这就像你家的仓库里有 100 万个箱子,现在你要清点所有箱子。如果你让一个工人一次性把所有箱子搬出来看一遍,那他得忙一整天,期间谁也进不来仓库。而 SCAN 就像把这个任务拆成每天搬 100 个箱子,每天完成一点,不会让仓库停工。

所以,在生产环境中,永远不要使用 KEYS *SCAN 才是正确姿势。


SCAN 命令的基本语法与工作原理

SCAN 命令的语法如下:

SCAN cursor [MATCH pattern] [COUNT count]
  • cursor:游标,用于记录遍历进度。初始值为 0,表示从头开始。
  • MATCH pattern:可选的匹配模式,例如 user:* 只匹配以 user: 开头的键。
  • COUNT count:建议的返回数量,但 Redis 不保证精确返回这么多,只是作为一个提示。

工作流程解析

SCAN 的工作方式是“分批遍历”:

  1. 你从 cursor = 0 开始调用 SCAN 0 MATCH user:* COUNT 10
  2. Redis 返回一批结果(比如 10 个键)和一个新的 cursor 值(比如 1234)。
  3. 你下次用 cursor = 1234 再调用 SCAN,继续获取下一批。
  4. cursor 返回 0 时,表示遍历结束。

这个过程就像你用一个“进度条”去翻一本厚厚的书,每次翻 10 页,记下当前页码,下次接着从那一页开始。这样不会卡死,还能随时暂停。


实际案例:使用 SCAN 批量删除特定前缀的键

假设你有一个用户系统,所有用户信息都以 user:123 的形式存储。现在你想清理所有 user: 开头的键,但数据量巨大,不能用 KEYS

我们用 SCAN 配合 DEL 命令来安全删除:

SCAN 0 MATCH user:* COUNT 100

返回结果示例:

1) "1234"
2) 1) "user:1001"
   2) "user:1002"
   3) "user:1003"
   4) "user:1004"
   5) "user:1005"

我们拿到了 5 个键和新的游标 1234。接下来我们删除这些键:

DEL user:1001 user:1002 user:1003 user:1004 user:1005

然后继续用 cursor = 1234 继续扫描:

SCAN 1234 MATCH user:* COUNT 100

重复这个过程,直到 cursor 返回 0。这样,整个删除过程是渐进式的,不会阻塞 Redis。

💡 提示:你可以写一个脚本(如 Bash、Python)自动完成这个循环,避免手动操作。


COUNT 参数的含义与调优建议

COUNT 参数是 SCAN 命令中非常关键的一个选项。它表示“希望 Redis 返回大约多少个键”。但请注意:

  • Redis 不保证返回 exactly COUNT 个结果。
  • 实际返回数量取决于键空间的分布、哈希表的负载情况。
  • 设置太小(如 COUNT 1),会导致遍历次数过多,增加网络开销。
  • 设置太大(如 COUNT 10000),可能单次返回过多数据,导致 Redis 暂时响应变慢。

推荐设置

COUNT 值 适用场景 说明
100 一般场景 平衡性能与延迟,推荐默认值
1000 数据量大 减少调用次数,但需注意阻塞风险
10 小数据集 适合测试或低并发环境

⚠️ 一般建议使用 COUNT 100 作为起点,根据实际负载调整。


SCAN 与 SSCAN、HSCAN、ZSCAN 的关系

SCAN 是 Redis 提供的通用遍历命令,它支持对不同数据结构进行遍历。你还可以使用:

  • SSCAN key [MATCH pattern] [COUNT count]:遍历集合(Set)中的成员。
  • HSCAN key [MATCH pattern] [COUNT count]:遍历哈希表(Hash)中的字段。
  • ZSCAN key [MATCH pattern] [COUNT count]:遍历有序集合(ZSet)中的成员。

它们的语法和工作原理与 SCAN 完全一致,只是作用对象不同。

举例:遍历集合中的元素

SADD user:1001:tags "admin" "vip" "active"

SSCAN user:1001:tags 0 MATCH * COUNT 10

返回结果:

1) "0"
2) 1) "admin"
   2) "vip"
   3) "active"

✅ 无论你是遍历键、集合、哈希还是有序集合,SCAN 系列命令都提供了统一的渐进式遍历能力。


常见问题与注意事项

1. 是否会漏掉或重复键?

SCAN 的设计保证了不会漏掉键,但可能重复。这是因为它基于哈希表的迭代机制,如果键在遍历过程中被修改(比如删除、新增),可能会被重复返回。不过这种概率很低,且在大多数场景下可以接受。

2. 如何判断遍历是否完成?

SCAN 命令返回的游标为 0 时,表示所有数据已遍历完毕。你不需要再调用。

3. 能否在遍历中修改数据?

可以,但不推荐。SCAN 本身是只读的,但你可以在遍历过程中执行 DELSET 等操作。不过要注意:

  • 删除正在遍历的键,不会影响遍历过程。
  • 增加新键,可能被后续的 SCAN 操作捕获。

4. 安全性:SCAN 会阻塞吗?

不会。SCAN 的设计初衷就是非阻塞的,即使数据量巨大,也不会导致 Redis 停顿。这是它相比 KEYS * 的最大优势。


实用脚本示例:Python 自动遍历删除

下面是一个 Python 脚本,使用 redis-py 库自动遍历并删除 user:* 前缀的键:

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

cursor = 0

while True:
    # 执行 SCAN 命令,每次获取 100 个键
    cursor, keys = r.scan(cursor=cursor, match='user:*', count=100)
    
    # 如果游标为 0,说明遍历完成
    if cursor == 0:
        print("遍历完成,所有 user:* 键已删除。")
        break

    # 删除这批键
    if keys:
        r.delete(*keys)  # *keys 展开为多个参数
        print(f"已删除 {len(keys)} 个键。")

print("任务完成。")

📌 这个脚本可以安全运行在生产环境,不会阻塞 Redis。


总结

Redis SCAN 命令 是 Redis 高级运维和开发中不可或缺的工具。它解决了 KEYS * 的阻塞问题,提供了安全、高效、渐进式的遍历方式。无论你是做数据清理、迁移、监控还是调试,掌握 SCAN 都能让你的系统更稳定、更健壮。

记住:

  • 永远不要用 KEYS *,尤其在生产环境。
  • SCAN 配合游标,分批处理数据。
  • 合理设置 COUNT,平衡性能与延迟。
  • 用脚本自动处理,提升效率。

当你在面对百万级键空间时,SCAN 就是你最可靠的伙伴。它不喧哗,却默默守护着系统的稳定运行。

下次你再遇到“遍历键”的需求,别再用 KEYS * 了,试试 SCAN,你会发现,原来优雅的解决方案,就在你身边。