
如果你问我Redis里最不起眼、却最容易踩坑的命令是哪一类我会说是键命令。刚带项目那阵子一个同事直接在线上执行了KEYS user:*结果Redis单线程被全量扫描卡住几秒钟内所有请求都排起了长队监控告警响成一片。从那以后我就意识到查找键、判断键是否存在、查看键值类型、删除键值、设置过期时间、查看有效时间、永久有效、移动键、改键名这些基础操作平时看着简单真正用错的时候分分钟出事故。今天我把日常开发中最常用的Redis键命令完整过一遍包含命令格式、实际执行效果、底层原理和线上经验。不管是刚接触Redis的新人还是想系统梳理键命令的老手这份清单都值得收藏。1. 键命令整体拆解先搞懂这些命令解决了什么问题在动手敲命令之前我习惯先把问题分类。Redis是个key-value存储系统所有的数据操作几乎都要从key开始。虽然日常开发中我们用得最多的是GET、SET这类数据命令但键命令才是真正管住key生命周期的工具。无论你做缓存、分布式锁、计数器还是排行榜最终都会遇到需要查找某个key、判断它存不存在、看它是什么类型、给它设个过期时间、甚至把它挪个位置的情况。1.1 为什么键命令是Redis操作的地基刚学Redis的时候很多人喜欢死磕五种数据结构反而忽略了键命令。其实键命令更像是一把万能钥匙。举个例子线上排查问题时线上有个key一直占着内存你首先得知道它是什么类型才能决定用LPOP还是SREM去清理缓存穿透时你要快速判断某个key是否存在、是否设置了过期时间数据误删后恢复时你又得知道RENAME和MOVE到底怎么用才不会把好数据覆盖掉。另外键命令还能帮你理解Redis的底层机制。通过TTL返回值的差异可以判断一个key是还没设置过期时间还是已经过期被删掉了通过UNLINK和DEL的对比可以理解Redis单线程模型的阻塞风险。这些基础不熟练掌握后续排查问题会非常吃力。1.2 键命令的分类和选型思路我习惯把常用的键命令分成几组查找键用KEYS和SCAN判断键是否存在用EXISTS查看键值类型用TYPE删除键值用DEL和UNLINK设置和查看过期时间用EXPIRE、TTL、PERSIST移动键和改名用MOVE、RENAME、RENAMENX。分组之后脑子里就有一条清晰的链路这个key在哪、在不在、是什么、怎么删、什么时候过期、怎么挪位置。选型的时候有一条核心原则生产环境优先选不阻塞的命令。KEYS和DEL都有可能在大数据量下卡住Redis主线程所以能用SCAN代替KEYS就用SCAN能选UNLINK就不用DEL删大key。至于MOVE这种跨库操作现在Redis Cluster环境下基本用不上但单机Redis运维时偶尔会用到还是要懂原理。2. 查找键与存在性判断KEYS、SCAN、EXISTS这样用才安全很多新手第一次接触Redis键命令就是从KEYS开始的。一条KEYS *就能把所有键列出来确实很爽。但爽完之后线上性能问题也就来了。这一节重点讲讲查找键和判断键存在性的正确姿势尤其是KEYS和SCAN的取舍。2.1 KEYS命令的模式匹配与隐藏风险KEYS命令的语法很简单KEYS pattern支持glob风格的通配符比如*匹配任意多个字符?匹配一个字符[]匹配字符集合。举个例子127.0.0.1:6379 MSET user:1001 tom user:1002 jerry order:10001 apple OK 127.0.0.1:6379 KEYS user:* 1) user:1001 2) user:1002 127.0.0.1:6379 KEYS user:100? 1) user:1001 2) user:1002在键数量很少的开发环境里KEYS确实很方便。但它的实现是全量扫描所有键然后逐个匹配模式复杂度是O(N)N是库里的键总数。Redis是单线程模型执行KEYS期间其他命令全部排队等待。一旦线上键数量达到百万甚至千万级一条KEYS就能让服务抖上好一阵子。我踩过的最痛的一次坑就是同事把KEYS user:*输成了KEYS *线上Redis直接阻塞了3秒多。从那以后我在项目里会主动用rename-command KEYS 把KEYS命令禁掉或者明确要求只在从节点或测试环境使用。如果只是想看少量键是否匹配建议用SCAN替代。2.2 用SCAN安全遍历所有键SCAN命令是KEYS的安全替代方案它的核心思路是游标式分批遍历。每次调用SCANRedis会返回一个游标和一批键下次把返回的游标再传给SCAN直到游标变为0表示遍历完成。127.0.0.1:6379 SCAN 0 MATCH user:* COUNT 100 1) 1536 2) 1) user:1001 2) user:1002 127.0.0.1:6379 SCAN 1536 MATCH user:* COUNT 100 1) 0 2) 1) user:1003这里有两个容易被误解的地方。第一COUNT 100不是保证返回100个键而是提示Redis本次扫描的工作量真正返回多少键取决于哈希表的分桶情况可能多也可能少。第二MATCH过滤虽然是在服务端做的但在游标遍历过程中如果键集合发生变化可能返回一些在当前条件下已经不匹配的键所以严谨的做法是在客户端再做一次过滤。用代码实现SCAN全量遍历也很简单以Python为例import redis r redis.Redis(decode_responsesTrue) cursor 0 while True: cursor, keys r.scan(cursorcursor, matchuser:*, count100) for key in keys: print(key) if cursor 0: breakSCAN不会像KEYS那样阻塞整个Redis因为它每次只遍历一小部分桶。但要注意它在遍历过程中不保证键的完整性同一个键可能被返回多次。如果业务上需要精确的去重结果可以在客户端用集合处理。2.3 EXISTS判断键值是否存在EXISTS是判断键是否存在的最快方式语法是EXISTS key [key ...]可以一次传入多个键。返回值是实际存在的键的个数而不是简单的0或1。127.0.0.1:6379 EXISTS user:1001 user:1002 order:10001 (integer) 3 127.0.0.1:6379 EXISTS user:9999 (integer) 0批量判断的好处是减少网络往返。如果不批量三个键要发三次命令用了EXISTS a b c一次就能拿到结果。实际项目中我经常用它做缓存键的批量健康检查。需要注意一点EXISTS只代表键存在不代表值里有有效数据。比如SET user:1001 存了一个空字符串EXISTS user:1001仍然返回1。所以在业务上判断数据是否有效时还得结合GET或TYPE来确认。另外如果一个键的过期时间已经到了EXISTS同样返回0因为Redis在访问时会先执行惰性删除。3. 查看键值类型与删除键值TYPE、DEL、UNLINK的取舍很多时候我们拿到一个陌生的key第一反应是直接用GET看看里面是什么。但如果你对一个哈希类型的key执行GETRedis会直接报错。所以排查问题前第一步应该是看这个key到底是什么类型再决定用哪一套数据命令。3.1 TYPE命令查看键值类型TYPE命令的语法是TYPE key返回值包括string、list、set、zset、hash、stream等。如果key不存在返回none。127.0.0.1:6379 TYPE user:1001 string 127.0.0.1:6379 TYPE order:10001 hash 127.0.0.1:6379 TYPE not_exist_key none知道类型之后才能选择合适的操作命令。比如上面order:10001是hash类型那就可以用HGETALL查看内容如果是list类型就可能需要LRANGE。对于排查线上问题来说TYPE还能帮你快速判断是不是key被其他线程写成了意料之外的类型。如果你还想再深入一点可以用OBJECT ENCODING key查看内部编码。同样是string小整数可能编码成int短字符串可能是embstr长字符串变成raw。这在分析内存占用时很有帮助。不过它属于进阶命令日常排查够用就好。3.2 DEL删除键值别忘了大键阻塞问题删除键值最直接的命令是DEL语法是DEL key [key ...]返回值是实际删除的键的个数。127.0.0.1:6379 DEL user:1001 user:1002 (integer) 2DEL本身是同步删除也就是说删除操作由主线程执行只有当键被从内存中释放后命令才返回。对于大多数小键这个操作飞快。但问题出在大键上比如一个set里有几千万个元素DEL会遍历并释放所有元素内存耗时可能达到秒级这时候Redis主线程就被卡住了。我之前遇到过线上一个超大list大概有1000多万个元素运维直接执行DEL监控上Redis的耗时瞬间飙红。后来我们改成用UNLINK立刻就解决了。3.3 UNLINK异步删除大键场景的救星UNLINK是Redis 4.0引入的异步删除命令语法和DEL一致也是UNLINK key [key ...]。它和DEL最大的区别是主线程先把key从键空间中摘除然后立刻返回真正的内存释放在后台线程慢慢执行。这样即使删除的是大key也不会长时间阻塞主流程。127.0.0.1:6379 UNLINK order:10001 (integer) 1需要注意的是UNLINK返回1只代表键已经被逻辑摘除不代表内存已经归还给操作系统。内存释放是异步的所以执行完UNLINK后通过INFO memory看到的used_memory可能不会立刻下降要稍微等一会儿。所以在批量清理数据、删除大key时我通常默认用UNLINK而不是DEL。对于小于一定阈值的小键UNLINK和DEL基本没有区别但大key场景下UNLINK明显更安全。4. 过期时间管理EXPIRE、TTL、PERSIST把临时和永久玩明白缓存系统里过期时间是个绕不开的话题。很多新手设置缓存时习惯先用SET写入再用EXPIRE设置过期时间这两条命令中间隔着一次网络往返存在很小的间隙。更优雅的做法是直接用SET key value EX seconds原子完成。这一节我们把过期时间相关的键命令一次讲透。4.1 设置过期时间的几种常用方式最基础的命令是EXPIRE key seconds把键的存活时间设置为指定秒数。比如设置一个验证码5分钟过期127.0.0.1:6379 SET code:9527 abc123 OK 127.0.0.1:6379 EXPIRE code:9527 300 (integer) 1除了EXPIRE还有几个变体PEXPIRE key milliseconds是按毫秒设置EXPIREAT key unix-timestamp是设置到某个绝对时间点PEXPIREAT对应毫秒版的时间戳。如果业务要求每天零点清空某个键计算好下一个零点的Unix时间戳用EXPIREAT就很合适。还有一种更省事的办法是在设置字符串类型的值时直接指定过期时间127.0.0.1:6379 SET code:9527 abc123 EX 300 OK这样一步到位避免SET和EXPIRE之间产生时间差。需要注意SET命令如果不带EX或PX参数覆盖写入同样键名的值时会清除之前设置的过期时间。这是新手最容易踩的坑之一。4.2 TTL和PTTL查看剩余有效时间TTL key返回键的剩余有效时间单位是秒。PTTL key单位是毫秒。理解TTL返回值非常重要因为它的返回含义有三层127.0.0.1:6379 TTL code:9527 (integer) 285 127.0.0.1:6379 TTL no_expire_key (integer) -1 127.0.0.1:6379 TTL expired_key (integer) -2第一返回正整数表示距离过期还剩多少秒第二返回-1表示这个键是永久有效的从没设置过过期时间第三返回-2表示键不存在或者已经过期被删除了。这三个状态必须记牢很多人把-2误当成永久有效结果排查半天。我通常会用PTTL做更精细的判断比如判断一个延迟队列中的key还有多久超时毫秒级精度会更准确。但日常监控用TTL就足够了。4.3 让键永久有效PERSIST清除过期时间如果之前给某个键设置了过期时间后来业务上又希望它永久保留可以用PERSIST key来移除过期时间。执行成功返回1如果键不存在或者原本就没有过期时间返回0。127.0.0.1:6379 PERSIST code:9527 (integer) 1 127.0.0.1:6379 TTL code:9527 (integer) -1注意PERSIST只是把过期时间去掉不会影响键值本身。还有一种常见场景是临时键过期时间快到了但业务还没处理完你会想给这个键续期。此时不需要先PERSIST再EXPIRE直接再执行一次EXPIRE key secondsRedis会用新的过期时间覆盖旧的。如果要给键设置一个很长的过期时间比如一年但没有永久有效这个词实际上可以用PERSIST做到永久。只是要注意缓存键长期不删会增加内存压力所以使用前要确认业务是否真的需要。4.4 过期删除策略的小知识前面说了TTL返回-2表示键已过期但过期键到底什么时候被删除很多人并不清楚。Redis采用两种策略配合惰性删除和定期删除。惰性删除是指当客户端访问一个键时Redis会先检查它是否已经过期如果过期就删除并返回空。定期删除则是Redis后台每隔一段时间随机抽取一部分带过期时间的键把其中已经过期的键删除。所以一个键即使到了过期时间如果一直不被访问它也不会立刻从内存中消失而是等定期删除扫到或者下次访问时才被清理。这就解释了一个线上常见现象明明给缓存设置了5分钟过期但5分钟过去后用MEMORY USAGE查看内存发现那个键好像还在。其实它只是还没被清理逻辑上你已经拿不到它了。如果业务对内存释放要求严格可以主动UNLINK但多数场景下等Redis自己的清理策略就够了。5. 键值移动与改名MOVE、RENAME、RENAMENX实操指南前面讲的都是键的查询和生命周期管理接下来我们处理键的“位置”和“名字”。这两个功能单独用起来不难但涉及数据覆盖、跨库移动等场景时坑也不少。5.1 MOVE跨库移动键MOVE key db可以把当前数据库中的某个键移动到目标数据库成功返回1失败返回0。例如默认0号库里有个user:1001把它移动到1号库127.0.0.1:6379 MOVE user:1001 1 (integer) 1如果目标库里已经存在同名的键MOVE会失败并返回0不会覆盖目标库中原有的键。这一点和RENAME不同相对安全。键被移动之后如果它设置了过期时间过期时间也会一起带走。不过在实际工程中我并不推荐用多个数据库来隔离业务。Redis的单机多db本质上只是用一个数字区分命名空间很多客户端连接时默认会使用0号库一旦忘记切换就会读到别的东西。再加上Redis Cluster模式完全不支持多db所以为了将来的扩展性更稳妥的做法是在键名上加业务前缀比如user:1001、order:10001。这样既能隔离数据又能在集群下无缝迁移。MOVE偶尔做做运维急救可以别把它当业务拆分工具。5.2 RENAME与RENAMENX改键名RENAME key newkey可以将键名修改为另一个名字。如果newkey已经存在RENAME会直接覆盖掉原来的值并且返回OK。如果源键不存在会返回错误。127.0.0.1:6379 SET user:1 tom OK 127.0.0.1:6379 RENAME user:1 user:1001 OK 127.0.0.1:6379 GET user:1001 tomRENAMENX key newkey则更谨慎它只有当newkey不存在时才会执行改名成功返回1如果newkey已经存在则返回0不会覆盖目标键。127.0.0.1:6379 RENAMENX user:1001 user:1002 (integer) 1在需要确保不覆盖已有键的场景下优先用RENAMENX。比如两个服务同时尝试把临时键改成正式键用RENAMENX就能避免互相覆盖。5.3 改名操作的依赖方问题改键名看起来只是一个命令但线上实际操作时一定要先想清楚有没有其他服务在依赖旧键名。如果某个服务还在用旧名字读缓存你突然改了键名轻则缓存命中率下降重则直接缓存穿透打到数据库。我处理过一次比较尴尬的事故一个上游服务把session:abc123改成了token:abc123但下游服务还在读session:abc123结果所有用户都重新登录了。后来我总结了一个规矩改键名前先搜索代码里有没有引用旧键名的地方必要时采用双写策略新旧键名同时写一段时间等所有调用方切完再删旧键。另外RENAME是原子操作不会出现改一半的情况。但它会覆盖目标键所以执行前先用EXISTS newkey确认目标键是否存在。如果目标键里有重要数据先备份或者用RENAMENX。6. 线上常见问题与排查技巧实录最后把我多年排查Redis键命令问题的经验整理成一份速查和避坑清单。这一部分不是理论全是从实际故障里总结出来的教训条条都有价值。6.1 KEYS命令引发的线上阻塞前文已经重点提过KEYS的危害这里再复盘一个真实场景。当时一个数据平台有大约800万个键同事想找出所有以report:开头的键直接在线上执行了KEYS report:*。结果Redis主线程被阻塞了近2秒期间所有读写请求全部堆积部分服务出现超时。解决办法很粗暴等命令执行完然后立刻在配置里禁用了KEYS命令。就算不是生产环境多键规模较大时也不要轻易用KEYS。习惯上我要求团队统一使用SCAN加MATCH来遍历键在客户端写一个循环函数避免任何人图方便直接敲KEYS *。6.2 过期时间设置无效或TTL返回值异常有个后端同学反馈他给一个缓存键设置了EXPIRE 300但第二天发现键还在。排查后发现他先执行了SET key value然后又执行了EXPIRE key 300这本身没问题。但没过多久另一个逻辑里又执行了一次SET key newValue这次覆盖把过期时间清掉了。Redis的SET在未指定EX参数时会移除原有过期时间所以一个键变成了永久键。解决办法也很简单要么所有写入都统一使用SET key value EX seconds要么在业务逻辑里避免对带过期时间的键做无脑SET覆盖。另外如果一个键的TTL长期是-1而你预期它应该自动过期就去代码里查一遍是否存在不带EX的SET。6.3 删除大键导致延迟与内存问题删除大键的坑主要是两个一个是耗时一个是内存没释放。耗时是因为DEL在主线程里同步释放内存大集合会造成秒级阻塞内存没释放是因为UNLINK把释放动作放到了后台客户端看到返回1后内存不会立刻下降。我现在的处理习惯是删除前先用MEMORY USAGE key估算键大小如果超过几十MB默认走UNLINK。如果线上内存很紧张又担心异步删除后的内存碎片那就用DEBUG OBJECT key的序列化长度和元素数量做参考必要时通过批量删除子元素的方式慢慢瘦身避免一次性释放过多内存导致延迟。6.4 图形客户端里的键命令操作很多人日常用Redis Desktop Manager或Another Redis Desktop Manager看数据图形界面其实也是调用了我们前面讲的这些命令。打开键列表时客户端底层大概率执行的是SCAN或KEYS所以如果你在一个几百万键的实例上使用图形客户端刷新键列表之前先确认它用的是SCAN模式否则同样会卡。在图形界面里查看一个键的过期时间通常会显示“TTL”栏目右键删除键会调用DEL或UNLINK。但图形客户端批量删除几千个键时可能是一条条删也可能是一次性传参删使用前留意一下客户端的实现避免不小心执行了超长命令。6.5 一份可以直接抄的避坑清单生产环境禁用KEYS统一用SCAN实现键遍历。删除大key优先用UNLINK小key用DEL没问题。设置字符串缓存时尽量用SET key value EX seconds避免SET和EXPIRE分两步。判断键是否存在用EXISTS判断类型用TYPE不要拿数据命令瞎试否则报错才发现类型不对。使用RENAME前先确认新名字是否已有重要数据防止直接覆盖。理解TTL返回-1是永久有效-2是不存在或已过期别搞混。MOVE跨库操作在Redis Cluster下无效建议用键名前缀做隔离。定期在大key巡检里加上MEMORY USAGE命令发现大key提前处理。这些键命令我几乎每天都会摸一遍。每次在线上排查问题的时候最后发现救命的往往不是那些炫酷的高级特性而是这些最基础、最容易被忽略的键操作。希望这份梳理能帮你少踩几个坑。