Redis SCAN命令替代KEYS的渐进式遍历原理
在生产环境中直接使用 KEYS 命令是导致 Redis 服务阻塞甚至瘫痪的常见原因。KEYS 命令会遍历整个数据库中的所有键,一旦键数量巨大(例如百万级别),Redis 的单线程特性会导致所有其他请求被挂起,等待遍历完成。SCAN 命令提供了一种非阻塞、渐进式的遍历方案,能够在不影响线上服务的前提下完成数据读取。
1. 理解 SCAN 的基本工作机制
SCAN 命令通过基于游标的迭代机制工作,它不是一次性返回所有匹配的键,而是分批返回。这意味着它每次只消耗少量的 CPU 时间,让 Redis 能够在两次迭代之间处理其他客户端的请求。
执行以下命令开始遍历:
SCAN 0 MATCH user:* COUNT 10
该命令包含三个核心参数:
0:游标,第一次遍历必须传0,表示从头开始。MATCH user:*:匹配规则,只返回满足user:*格式的键。COUNT 10:提示 Redis 每次迭代大概返回多少个键(仅是提示,不保证严格数量)。
观察命令返回结果:
1) "136" # 新的游标,不为 0 表示遍历未结束
2) 1) "user:1001" # 返回的键列表
2) "user:1005"
3) "user:1008"
记住核心判断逻辑:只要返回的第一个元素(新游标)不等于 0,就必须用这个新游标再次调用 SCAN 命令,直到游标返回 0。
2. 构建客户端遍历循环
为了实际获取所有数据,必须在应用程序代码中构建循环逻辑。以下是标准的处理流程。
编写 Python 代码实现上述逻辑:
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def scan_keys(pattern, count=10):
cursor = '0'
while cursor != 0:
# 执行 SCAN 命令
cursor, keys = r.scan(cursor=cursor, match=pattern, count=count)
# 处理当前批次获取到的键
for key in keys:
print(f"Found key: {key.decode('utf-8')}")
# 继续下一轮循环,直到 cursor 变为 0
# 使用示例
scan_keys('user:*')
注意代码中的循环条件:使用 while cursor != 0 确保遍历完整。
3. 深入底层原理:字典结构与遍历算法
SCAN 的高效性源于 Redis 对底层数据结构的优化。Redis 使用哈希表来存储键值对,SCAN 实际上是在遍历哈希表的数组槽位。
Redis 的哈希表类似于一个数组,每个位置是一个桶。SCAN 的遍历顺序并非按照数组索引 0, 1, 2... 递增,而是采用了一种特殊的顺序。
理解高位优先遍历:
假设哈希表数组大小为 8(二进制 1000),索引范围是 0 到 7。SCAN 的遍历顺序并不是按数字大小,而是按二进制位的反转顺序或者说是高位掩码移动的顺序。简而言之,它在扩容或缩容时能够最大限度地复用已经遍历过的位置,减少重复。
这解释了 SCAN 的两个重要特性:
- 无重复保证(在数据不变时):如果没有数据插入、删除或 Rehash(重新哈希),
SCAN能保证返回所有存在的元素且不会重复。 - 元素可能重复或缺失(在数据变化时):
- 如果在遍历过程中进行了 Rehash(扩容或缩容),同一个键可能从旧哈希表移动到了新哈希表,
SCAN可能会再次读取到它,导致返回重复键。客户端代码必须具备处理重复键的能力(例如使用 Set 去重)。 - 如果一个键在遍历过某个槽位后被插入到该槽位,且后续不再遍历该位置,这个键可能会被遗漏。
- 如果在遍历过程中进行了 Rehash(扩容或缩容),同一个键可能从旧哈希表移动到了新哈希表,
4. 关键参数的最佳实践
正确使用 COUNT 和 MATCH 参数对于性能至关重要。
调整 COUNT 参数
COUNT 默认值是 10。它不是硬性限制,而是告知 Redis 每次迭代需要做多少“工作”。
- 设置 较小的值(如
10或100):每次迭代耗时短,对 Redis 影响小,但完成全量遍历需要更多的网络往返(RTT),适合对延迟敏感的线上环境。 - 设置 较大的值(如
1000或5000):减少网络往返,加快遍历速度,但单次命令阻塞时间增加。
建议:在生产环境批量扫描大量数据时,可以从 COUNT 100 开始尝试,观察 Redis 的负载情况动态调整。
使用 MATCH 参数
MATCH 参数是在服务器端提取数据之后进行过滤的。这意味着,即使 MATCH 规则匹配不到任何键,Redis 依然需要遍历对应的槽位。
场景分析:如果数据库中有 1000 万个键,其中只有 10 个是以 user: 开头的。
执行 SCAN 0 MATCH user:* COUNT 1000 时,Redis 可能会遍历 1000 个桶,但最终返回空列表,因为桶里的键都不符合 user:*。
结论:MATCH 不会减少服务器端的遍历工作量(除非使用 Redis 6.0+ 的 lazy expire 优化等特定场景),它只减少网络传输的数据量。如果匹配集非常稀疏,遍历过程可能会感觉“很慢”且返回空结果多次,这是正常的。
5. 处理特殊情况
在使用 SCAN 替代 KEYS 时,必须处理好以下边界情况。
遍历大集合时的并发修改
假设你在遍历一个包含 100 万个键的数据库,同时有其他程序在不断地插入新键或删除旧键。
- 应对重复:在应用层使用
Set或Hash记录已处理的 ID。processed_ids = set() for key in keys: if key not in processed_ids: process(key) processed_ids.add(key) - 接受遗漏:对于实时性要求极高的统计,
SCAN不是最佳选择。但对于备份、分析等离线任务,偶尔的遗漏通常是可以接受的,或者可以通过第二次遍历来交叉验证。
空数据库或无匹配数据
当数据库为空或没有任何键匹配 MATCH 模式时,SCAN 会立即返回游标 0 和空列表。
验证逻辑:
SCAN 0 MATCH non_existent:*
# 返回
1) "0"
2) (empty list or set)
一旦在第一轮循环中拿到游标 0,循环应立即终止,避免死循环。
6. 总结替代方案
为了更直观地对比,请参考下表:
| 特性 | KEYS 命令 | SCAN 命令 |
|---|---|---|
| 时间复杂度 | O(N),N 为总键数 | O(1) 每次调用,完成全量扫描为 O(N) |
| 阻塞风险 | 高,遍历期间阻塞所有请求 | 低,分批执行,不长时间阻塞 |
| 遍历方式 | 一次性返回所有数据 | 基于游标,渐进式返回 |
| 数据一致性 | 快照式,结果准确 | 弱一致性,可能重复或遗漏 |
| 使用场景 | 开发/调试环境,数据量极小 | 生产环境,大数据量维护 |
执行替换操作时,请遵循以下步骤:
- 搜索代码库中所有包含
KEYS或redis.keys()的地方。 - 评估调用频率。如果是高频调用,必须优先处理。
- 重写逻辑,引入
while循环和游标处理机制。 - 测试去重逻辑,确保业务能容忍数据重复。
- 压测观察
COUNT参数对 Redis CPU 和延迟的影响。
通过将 KEYS 替换为 SCAN,可以彻底消除因运维操作导致的 Redis 阻塞风险,保障线上服务的稳定性。

暂无评论,快来抢沙发吧!