在Redis中查找一批符合特定命名规则的key,比如所有以user:开头的键,最直接的想法是用KEYS user:*。这条命令在小数据量下确实好用,但一旦数据量达到几十万甚至百万级别,它就可能成为线上事故的导火索。Redis从2.8版本开始提供了SCAN命令族,专门用于增量式迭代,是生产环境模糊查询的推荐方案。本文将从底层机制、性能对比和实际代码几个角度,把这两个命令讲清楚。

一、keys命令为什么危险:单线程阻塞机制分析
Redis的核心网络模型是单线程处理命令的(持久化和IO线程除外),这意味着任何一条执行缓慢的命令都会阻塞后续所有请求。KEYS pattern的时间复杂度是O(N),N是整个数据库中key的总数,而不仅仅是匹配数量。也就是说,哪怕你的模式只匹配到10个key,Redis也要把全库所有key遍历一遍才能返回结果。
更严重的问题在于遍历过程中Redis需要收集所有匹配结果一次性返回。假设库里有500万个key,匹配到200万个,返回的结果集会占用大量内存用于构造响应,同时网络传输这份大响应也需要时间。在这段时间内,Redis主线程完全被占用,业务请求排队超时,如果上层配置了缓存超时熔断,大量请求直接打到数据库,就可能引发缓存雪崩。
还有一个容易被忽略的点:KEYS遍历的是全局哈希表。当Redis正在做rehash(哈希表扩容)时,遍历成本还会进一步增加。因此官方文档明确警告,KEYS只应该用于调试环境,生产环境请改用SCAN。
二、scan命令的游标原理与增量迭代机制
SCAN cursor [MATCH pattern] [COUNT count]采用游标方式分批遍历。每次调用返回两部分:一个新的游标值和一批符合条件的元素。当返回的游标为0时,表示遍历完成。它的时间复杂度虽然是O(1)(单次调用),但整体遍历完整库仍然是O(N),区别在于每次只处理一小部分桶,处理完立即让出主线程,对其他请求的影响被摊薄了。
需要注意COUNT参数的含义:它是每次扫描的桶数量的提示值,不是精确返回的元素个数。默认值是10,实际生产中通常设置为几百到几千。设置太小会导致往返次数过多,设置太大又会退化成近似KEYS的行为,需要根据实际情况压测调整。
SCAN还提供两个重要保证:第一,遍历开始到结束期间一直存在的key,一定会在某次迭代中被返回,不会遗漏;第二,同一次完整遍历中,一个key不会被重复返回(除非在遍历期间被删除又重新写入)。但要理解,遍历期间新增的key可能返回也可能不返回,遍历期间删除的key可能还没被扫到就消失了。这种弱一致性对大多数运维场景是可以接受的。
在redis-cli中手动分批执行的示例:
redis-cli SCAN 0 MATCH user:* COUNT 1000 # 返回格式:1) "下一次游标值" 2) 1) "user:1001" 2) "user:1002" ... # 拿到游标继续调用,直到游标返回 0 表示遍历结束
三、实际性能对比与代码实现
在一台普通配置的实例上写入100万个key进行测试,KEYS user:*单次执行约耗时800毫秒到2秒(取决于值大小和是否rehash),期间其他请求的延迟会飙升至秒级甚至超时。而用SCAN配合COUNT=2000完整遍历同样数据,总耗时略高于KEYS(多了命令往返开销),但每次调用的响应时间稳定在1毫秒左右,业务请求几乎无感知。这就是两者的本质区别:总CPU开销接近,但阻塞分布完全不同。
用Python实现一个安全的全量扫描封装:
import redis
def scan_keys(r, pattern, count=2000):
"""使用 SCAN 增量遍历匹配的 key,避免阻塞 Redis"""
cursor = 0
result = []
while True:
cursor, keys = r.scan(cursor=cursor, match=pattern, count=count)
result.extend(keys)
if cursor == 0:
break
return result
r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)
# keys = scan_keys(r, 'user:*')
# print(f'共匹配到 {len(keys)} 个 key')
使用时还有几个细节要留意。如果业务要求精确一致的结果,应该配合具体业务加锁或使用SSCAN、HSCAN、ZSCAN遍历集合结构的成员,而不是依赖全库扫描。对于主从架构,SCAN可以放在从节点执行,进一步降低对写入路径的影响。另外,Python客户端返回的key可能是bytes类型,需要用decode_responses=True或手动decode处理。
四、选型建议与总结
数据量在一万以内、且是非高峰期的离线操作,用KEYS问题不大,代码简单直观。但任何面向线上服务的场景,无论当前数据量多小,都应该默认使用SCAN,因为数据量是会增长的,今天安全的一万key明年可能变成一百万。养成习惯的成本很低,换来的稳定性收益很高。
再补充一个折中方案:如果查询模式固定且频繁,可以考虑维护一个辅助集合,比如用SADD user_index user:1001把key名登记到Set中,查询时直接SMEMBERS user_index(数据量大时同样配合SSCAN),从设计上消除模糊扫描的需求,这是性能最好的方案。
总结一下核心结论:KEYS是O(N)全量阻塞扫描,只适合调试;SCAN通过游标增量遍历,单次开销小、不阻塞业务,是生产环境唯一推荐的模糊查询方式。理解两者的差异,本质上是要理解Redis单线程模型对慢命令的零容忍,这个思路同样适用于评估其他Redis命令能否上线。