Redis的KEYS命令大概是初学者最先学会、也是最容易在生产环境闯祸的命令之一。它能按照给定的模式匹配数据库中的键,用起来非常方便,但官方文档明确警告:不要在生产环境使用。这篇文章会把KEYS命令的语法、原理、风险以及替代方案讲透,帮助你在实际项目中做出正确选择。

一、Keys命令的基本语法与通配符用法
KEYS命令的语法非常简单,只接受一个参数pattern,用于匹配键名。返回值是所有匹配到的键名组成的数组。它支持三种通配符:?匹配任意单个字符,*匹配任意数量字符(包括零个),[abc]匹配方括号中的任意一个字符。例如KEYS user:*可以找出所有以user:开头的键,KEYS user:??:1001可以匹配user:zhang:1001这类固定长度的结构。
# 连接redis后执行 redis> KEYS * 1) "user:1001" 2) "order:2001" 3) "session:abc" redis> KEYS user:* 1) "user:1001" 2) "user:1002" redis> KEYS user:10[12][0-9] 1) "user:1001" 2) "user:1002"
还可以用\转义通配符本身,比如想匹配字面上的星号字符,可以写成KEYS user:\*。此外[a-c]这种范围写法也是支持的,表示a、b、c三个字符中的任意一个。需要注意的是,这些匹配是大小写敏感的,user和User会被视为完全不同的前缀。
从功能上看,KEYS似乎是个查找利器,但它的实现方式决定了它的性能特征:Redis是单线程处理命令的(忽略持久化线程等辅助线程),KEYS执行时需要遍历整个键空间,逐一与模式做匹配,并且把所有匹配结果一次性收集完才返回。这意味着不管匹配到多少个键,它都要把整个数据库扫一遍。
二、为什么生产环境要禁用Keys命令
KEYS的时间复杂度是O(N),N是数据库中的键总数。注意这里的N不是匹配结果的数量,而是全库键的数量。假设你的Redis里存了两百万个键,执行一次KEYS user:*,即使只匹配到十个键,Redis也必须遍历这两百万个键才能返回结果。
更严重的问题在于阻塞。Redis的主线程在执行KEYS期间无法处理其他任何命令,如果键数量庞大,这个命令可能耗时几百毫秒到几秒钟。在这段时间内,所有业务请求都会被排队等待,上游应用可能出现大量超时,进而引发连接堆积、缓存雪崩等连锁反应。很多线上事故的剧本都是相似的:运维想清理一批缓存,随手敲了个KEYS xxx*,结果整个服务瞬间不可用。
还有一个容易被忽视的问题是返回值体积。如果匹配到几十万个键,KEYS会一次性把这些键名全部序列化返回,这个响应可能达到几十MB甚至更大,传输过程中占用大量网络带宽和客户端内存。如果用Jedis这类同步客户端接收,还可能直接触发内存溢出。
正因为如此,很多公司的Redis使用规范里直接把KEYS列为禁用命令,云厂商如阿里云、腾讯云的Proxy架构Redis甚至会直接拦截这个命令。如果确实需要模糊查找能力,正确的设计方式是在写入键时同步维护一个Set作为索引,比如把所有user:1001格式的键名存进user:index这个集合里,查找时用SMEMBERS即可,复杂度可控且不阻塞。
三、安全替代方案:SCAN迭代遍历
从Redis 2.8.0开始,官方提供了SCAN命令作为KEYS的安全替代。它同样支持通配符模式匹配,但采用增量式迭代:每次调用只扫描键空间的一小部分并返回一个游标,客户端拿着游标再次调用,直到游标返回0表示遍历完成。
# SCAN cursor [MATCH pattern] [COUNT count] [TYPE type] redis> SCAN 0 MATCH user:* COUNT 100 1) "45" # 下一次迭代使用的游标 2) 1) "user:1001" 2) "user:1088" redis> SCAN 45 MATCH user:* COUNT 100 1) "0" # 游标为0表示遍历结束 2) 1) "user:2003"
SCAN的核心优势在于每次只做少量工作就交还CPU,让Redis有机会处理其他命令,整个过程不会造成明显阻塞。COUNT参数只是每次扫描的期望数量提示,并非精确值,默认为10,生产环境通常调到几百以平衡效率和平滑度。在代码中用一个循环即可完成完整遍历,以Java的Jedis为例:
ScanParams params = new ScanParams();
params.match("user:*");
params.count(500);
String cursor = ScanParams.SCAN_POINTER_START;
List<String> allKeys = new ArrayList<>();
do {
ScanResult<String> result = jedis.scan(cursor, params);
allKeys.addAll(result.getResult());
cursor = result.getCursor();
} while (!"0".equals(cursor));
使用SCAN时有两个注意点。第一,它保证遍历开始到结束期间一直存在的键至少被返回一次,但可能在同一次遍历中重复返回某个键,所以业务代码要做去重或幂等处理。第二,遍历过程中新增或删除的键是否被返回是不确定的,如果需要强一致性快照,SCAN无法满足,只能考虑在从库上执行或使用其他设计。
另外,SCAN还有几个面向特定数据结构的兄弟命令:SSCAN遍历集合、HSCAN遍历哈希、ZSCAN遍历有序集合。这些命令的存在也是为了解决大集合上执行SMEMBERS、HGETALL、ZRANGE全量读取时的阻塞问题,使用思路与SCAN完全一致。
四、常见问题解答
问题1:Keys执行很慢或者卡住了怎么办?如果在执行中无法中断,只能等待其完成,Redis没有提供终止正在执行的命令的手段。这也是建议配置lazyfree-lazy-expire和合理命令超时监控的原因。事后应立刻把客户端的这类调用改为SCAN方案。
问题2:如何安全地批量删除匹配的键?先通过SCAN迭代拿到键名,再分批调用UNLINK删除。UNLINK与DEL的区别在于它只从键空间摘除键,实际的内存回收交给后台线程异步完成,删除大键时不会长时间阻塞主线程。
redis> SCAN 0 MATCH session:* COUNT 200 1) "127" 2) 1) "session:abc" 2) "session:def" redis> UNLINK session:abc session:def (integer) 2
问题3:KEYS和SCAN的结果一样吗?功能上都做模式匹配,但KEYS返回全量精确结果,SCAN是分批可能重复的结果,需客户端聚合去重。性能上一个阻塞一个不阻塞,生产环境一律选SCAN。
问题4:为什么有时KEYS明明很快?键数量少的时候遍历耗时极短,测试环境感觉不到问题,但键规模上去后风险是指数级放大的,这也是很多事故发生在业务量增长之后的原因。
总结一下:KEYS命令适合在开发调试或本地环境快速查看键,生产环境一律用SCAN替代遍历,用UNLINK替代DEL删除批量键。更根本的解法是在数据设计阶段就维护好索引结构,避免对模糊查找的依赖,从源头消除这类性能隐患。
Redis Keys命令SCAN遍历Redis键管理修改时间:2026-09-05 19:52:53