导读:本期聚焦于会飞的猪创作的《Redis模糊匹配key用keys还是scan?性能差异与避坑实践详解》,敬请观看详情。线上Redis执行一次keys命令为什么会导致服务卡顿甚至雪崩?这篇内容从单线程阻塞机制出发,对比keys与scan在时间复杂度、阻塞时长和内存开销上的差异,并用实测数据说明百万级key场景下两者的性能表现。文中详细讲解scan的游标原理、COUNT参数设置、保证元素完整性的机制,以及Python和redis-cli的实现示例,同时分析scan可能漏key的场景和重复返回问题的应对方式,最后给出不同数据规模下的选型建议,帮助你避免生产环境的模糊查询事故。

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

Redis模糊匹配key用keys还是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')

使用时还有几个细节要留意。如果业务要求精确一致的结果,应该配合具体业务加锁或使用SSCANHSCANZSCAN遍历集合结构的成员,而不是依赖全库扫描。对于主从架构,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命令能否上线。

Redis模糊查询keys命令scan命令修改时间:2026-09-01 11:12:54

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。