Redis键命令完全指南:从KEYS到UNLINK的避坑实战
2026/9/18 12:07:15 网站建设 项目流程

如果你问我Redis里最不起眼、却最容易踩坑的命令是哪一类,我会说是键命令。刚带项目那阵子,一个同事直接在线上执行了KEYS user:*,结果Redis单线程被全量扫描卡住,几秒钟内所有请求都排起了长队,监控告警响成一片。从那以后我就意识到,查找键、判断键是否存在、查看键值类型、删除键值、设置过期时间、查看有效时间、永久有效、移动键、改键名这些基础操作,平时看着简单,真正用错的时候分分钟出事故。

今天我把日常开发中最常用的Redis键命令完整过一遍,包含命令格式、实际执行效果、底层原理和线上经验。不管是刚接触Redis的新人,还是想系统梳理键命令的老手,这份清单都值得收藏。

1. 键命令整体拆解:先搞懂这些命令解决了什么问题

在动手敲命令之前,我习惯先把问题分类。Redis是个key-value存储系统,所有的数据操作几乎都要从key开始。虽然日常开发中我们用得最多的是GETSET这类数据命令,但键命令才是真正管住key生命周期的工具。无论你做缓存、分布式锁、计数器还是排行榜,最终都会遇到需要查找某个key、判断它存不存在、看它是什么类型、给它设个过期时间、甚至把它挪个位置的情况。

1.1 为什么键命令是Redis操作的地基

刚学Redis的时候,很多人喜欢死磕五种数据结构,反而忽略了键命令。其实键命令更像是一把万能钥匙。举个例子,线上排查问题时,线上有个key一直占着内存,你首先得知道它是什么类型,才能决定用LPOP还是SREM去清理;缓存穿透时,你要快速判断某个key是否存在、是否设置了过期时间;数据误删后恢复时,你又得知道RENAMEMOVE到底怎么用才不会把好数据覆盖掉。

另外,键命令还能帮你理解Redis的底层机制。通过TTL返回值的差异,可以判断一个key是还没设置过期时间,还是已经过期被删掉了;通过UNLINKDEL的对比,可以理解Redis单线程模型的阻塞风险。这些基础,不熟练掌握,后续排查问题会非常吃力。

1.2 键命令的分类和选型思路

我习惯把常用的键命令分成几组:查找键用KEYSSCAN,判断键是否存在用EXISTS,查看键值类型用TYPE,删除键值用DELUNLINK,设置和查看过期时间用EXPIRETTLPERSIST,移动键和改名用MOVERENAMERENAMENX。分组之后,脑子里就有一条清晰的链路:这个key在哪、在不在、是什么、怎么删、什么时候过期、怎么挪位置。

选型的时候有一条核心原则:生产环境优先选不阻塞的命令。KEYSDEL都有可能在大数据量下卡住Redis主线程,所以能用SCAN代替KEYS就用SCAN,能选UNLINK就不用DEL删大key。至于MOVE这种跨库操作,现在Redis Cluster环境下基本用不上,但单机Redis运维时偶尔会用到,还是要懂原理。

2. 查找键与存在性判断:KEYS、SCAN、EXISTS这样用才安全

很多新手第一次接触Redis键命令,就是从KEYS开始的。一条KEYS *就能把所有键列出来,确实很爽。但爽完之后,线上性能问题也就来了。这一节重点讲讲查找键和判断键存在性的正确姿势,尤其是KEYSSCAN的取舍。

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的安全替代方案,它的核心思路是游标式分批遍历。每次调用SCAN,Redis会返回一个游标和一批键,下次把返回的游标再传给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_responses=True) cursor = 0 while True: cursor, keys = r.scan(cursor=cursor, match="user:*", count=100) for key in keys: print(key) if cursor == 0: break

SCAN不会像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。所以在业务上判断数据是否有效时,还得结合GETTYPE来确认。另外,如果一个键的过期时间已经到了,EXISTS同样返回0,因为Redis在访问时会先执行惰性删除。

3. 查看键值类型与删除键值:TYPE、DEL、UNLINK的取舍

很多时候我们拿到一个陌生的key,第一反应是直接用GET看看里面是什么。但如果你对一个哈希类型的key执行GET,Redis会直接报错。所以排查问题前,第一步应该是看这个key到底是什么类型,再决定用哪一套数据命令。

3.1 TYPE命令查看键值类型

TYPE命令的语法是TYPE key,返回值包括stringlistsetzsethashstream等。如果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) 2

DEL本身是同步删除,也就是说删除操作由主线程执行,只有当键被从内存中释放后,命令才返回。对于大多数小键,这个操作飞快。但问题出在大键上,比如一个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。对于小于一定阈值的小键,UNLINKDEL基本没有区别,但大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

这样一步到位,避免SETEXPIRE之间产生时间差。需要注意,SET命令如果不带EXPX参数,覆盖写入同样键名的值时,会清除之前设置的过期时间。这是新手最容易踩的坑之一。

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只是把过期时间去掉,不会影响键值本身。还有一种常见场景是,临时键过期时间快到了,但业务还没处理完,你会想给这个键续期。此时不需要先PERSISTEXPIRE,直接再执行一次EXPIRE key seconds,Redis会用新的过期时间覆盖旧的。

如果要给键设置一个很长的过期时间,比如一年,但没有永久有效这个词,实际上可以用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:1001order: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 "tom"

RENAMENX 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。习惯上,我要求团队统一使用SCANMATCH来遍历键,在客户端写一个循环函数,避免任何人图方便直接敲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,而你预期它应该自动过期,就去代码里查一遍是否存在不带EXSET

6.3 删除大键导致延迟与内存问题

删除大键的坑主要是两个,一个是耗时,一个是内存没释放。耗时是因为DEL在主线程里同步释放内存,大集合会造成秒级阻塞;内存没释放是因为UNLINK把释放动作放到了后台,客户端看到返回1后内存不会立刻下降。

我现在的处理习惯是:删除前先用MEMORY USAGE key估算键大小,如果超过几十MB,默认走UNLINK。如果线上内存很紧张,又担心异步删除后的内存碎片,那就用DEBUG OBJECT key的序列化长度和元素数量做参考,必要时通过批量删除子元素的方式慢慢瘦身,避免一次性释放过多内存导致延迟。

6.4 图形客户端里的键命令操作

很多人日常用Redis Desktop Manager或Another Redis Desktop Manager看数据,图形界面其实也是调用了我们前面讲的这些命令。打开键列表时,客户端底层大概率执行的是SCANKEYS,所以如果你在一个几百万键的实例上使用图形客户端,刷新键列表之前先确认它用的是SCAN模式,否则同样会卡。

在图形界面里查看一个键的过期时间,通常会显示“TTL”栏目,右键删除键会调用DELUNLINK。但图形客户端批量删除几千个键时,可能是一条条删,也可能是一次性传参删,使用前留意一下客户端的实现,避免不小心执行了超长命令。

6.5 一份可以直接抄的避坑清单

  • 生产环境禁用KEYS,统一用SCAN实现键遍历。
  • 删除大key优先用UNLINK,小key用DEL没问题。
  • 设置字符串缓存时,尽量用SET key value EX seconds,避免SETEXPIRE分两步。
  • 判断键是否存在用EXISTS,判断类型用TYPE,不要拿数据命令瞎试,否则报错才发现类型不对。
  • 使用RENAME前先确认新名字是否已有重要数据,防止直接覆盖。
  • 理解TTL返回-1是永久有效,-2是不存在或已过期,别搞混。
  • MOVE跨库操作在Redis Cluster下无效,建议用键名前缀做隔离。
  • 定期在大key巡检里加上MEMORY USAGE命令,发现大key提前处理。

这些键命令,我几乎每天都会摸一遍。每次在线上排查问题的时候,最后发现救命的往往不是那些炫酷的高级特性,而是这些最基础、最容易被忽略的键操作。希望这份梳理能帮你少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询