很多人学Redis,习惯打开官方命令手册,一条一条去试,今天试个SET,明天试个LPUSH。看的时候感觉全看懂了,真到线上出问题,脑子里翻遍命令文档也想不起该用哪条命令救火。这种学法不是不对,是太低效。我做了这么多年的后端开发和系统维护,Redis命令前前后后摸过上百条,真正在项目里高频使用的其实就是一个很小的子集。这篇文章不打算把所有命令罗列一遍,那没有意义,我想换个拆法:从数据模型讲到命令语义,从命令语义讲到实操排障,把我自己趟过的坑和积累下来的经验一并说透。
先说一个很多人容易忽略的底层认知:Redis的命令不是随随便便拼出来的,每一条命令的设计,都对应着一种数据结构的操作方式和业务场景。你理解了String、Hash、List、Set、ZSet这些底层数据模型之后,再看命令,就会发现它们不是零散的一百多个动词,而是一套有序的"数据结构操作接口"。写代码的时候你可能感受不到这种区别,但等你排查线上问题,需要在不看文档的情况下精准敲出正确命令,这种认知就变得极其关键。
1. Redis命令的分类框架:先建立自己的认知地图
1.1 两类维度看命令:按操作对象和按能力范围
学Redis命令之前,我建议你先做一个分类,不要一上来就记KILL、CONFIG这种零散的运维命令。第一类维度是按操作对象,也就是数据类型来分,这对应你去操作某个键的值。比如String类型的SET、GET、INCR,Hash类型的HSET、HGET、HGETALL,List类型的RPUSH、LPOP、LRANGE。第二类维度是按能力范围分,包括操作键本身生命周期和属性的通用命令,比如EXPIRE、TTL、TYPE、DEL;负责服务端配置和运行信息查询的,比如CONFIG GET、INFO、DBSIZE;还有发布订阅、事务、脚本、慢日志这一类支撑型能力。
我见过不少刚入行的朋友,喜欢啃完整命令列表,一遍过完甚至能背下来几十条。但真要写业务代码的时候,还是只会用SET和GET。为什么?因为他是按字母序去背的,没有建立"遇到什么场景应该往哪个方向去查命令"的意识。我建议你把第一类维度作为重点,因为线上99%的读写都发生在这几个核心数据类型上,第二类维度按需补漏,等到你真正排查问题的时候再去补充。
1.2 Keys命令和Scan命令的实际区别:不只是性能问题
在全局操作维度中,有一个命令值得单独拎出来讲:KEYS。很多教学文章喜欢用KEYS做演示,因为简单,一眼能看到所有键。但在生产环境,KEYS是一个名副其实的"危险品"。这个命令会遍历整个键空间,如果Redis实例里有几百万甚至上千万个键,它会阻塞住整个Redis进程,期间所有读写请求都会卡住。很多线上故障就是因为有人深夜在Redis里敲了一条KEYS *,直接把服务"打没"了。
正确做法是使用SCAN命令。SCAN采用的是游标迭代方案,每次返回一小批键,配合游标参数循环取完,不会长时间阻塞服务。我自己的习惯是写一个小脚本,用循环SCAN去统计键的数量、找特定前缀的键,都通过这个方案来做。虽然代码比KEYS多几行,但对线上服务是零风险的。
注意:凡是要遍历所有Redis键的场景,第一反应应该是SCAN而不是KEYS,这不是性能问题,是可用性问题。尤其不要用KEYS去匹配"user:*"这类带前缀的大量键,扫一遍就可能引发雪崩。
2. 五大核心数据类型的命令实战:背场景而不是背语法
2.1 String类型:不只是缓存字符串那么简单
String类型是所有Redis数据类型里最简单,同时也是最容易被用窄的一个类型。说它简单,是因为它的值本质就是一个字节数组,能存字符串、数字、序列化对象。说它容易被用窄,是因为很多人除了SET、GET之外,完全不知道这个类型里还有一批"原子操作"命令,能直接解决并发问题。
举一个最典型的场景:计数。业务上经常需要做访问统计、限流计数、点赞数累加这类需求。如果你先GET出来,在代码里加1,再SET回去,在高并发下一定会丢更新。正确做法是直接使用INCR命令,把这个自增动作交给Redis完成。Redis是单线程处理命令的,INCR本身就是原子操作,不会出现并发覆盖问题。类似的还有DECR、INCRBY,需要增加指定数量时直接用INCRBY。这些命令在秒杀、库存扣减、计数器场景里非常趁手。
还有一个实战细节,SET命令不只是"存键值"三个字这么简单。SET支持NX和XX参数,NX表示只有当键不存在时才设置成功,XX表示只有当键存在时才设置成功。这就是分布式锁最底层的原语。后面我会专门讲如何用命令组合设计分布式锁,这里先记住,SET NX EX这种组合能在一个命令里实现加锁和过期时间设置,避免了分步执行可能产生的死锁风险。此外,SET的EX和PX参数支持秒级和毫秒级过期,这是很多业务场景比EXPIRE命令更好用的地方,因为原子性更好。
GETSET命令很多人不了解,它能在获取旧值的同时设置新值。在统计系统里需要刷新某个key再拿到上一轮数据的场景,这段操作单命令就被解决了。GETEX、GETDEL这类命令则是对过期和删除场景的补齐,一旦你开始注意操作原子性,会发现这些命令总能在恰当的地方用上。
2.2 Hash类型:处理对象类数据的最佳选择
Hash类型是一个键下面挂了多个字段,适合存储对象的多个属性。很多人在存用户信息、商品信息的时候,习惯把整个对象序列化成JSON塞进String。这种做法一旦需要修改对象里的一个字段,就得把整个对象取出来反序列化,改完再序列化返回,代价大且容易出并发问题。Hash类型用HSET命令可以直接设置某个字段,用HGET直接取某个字段,用HINCRBY对指定字段做原子自增,天然适合这种场景。
举个例子,一个用户维度有用户ID、用户名、头像、积分、等级。你完全可以用HSET user:1001 name "某开发者" score 100 level 3,一条命令初始化多个字段。后续用户积分变动只需要HINCRBY user:1001 score 50,不用把整条用户记录拉出来。等到要显示用户卡片信息的时候,HGETALL一把梭,把所有字段取出来。读取单个字段用HGET,需要多字段用HMGET,非常灵活。
Hash在性能上有两个事项值得注意。第一是哈希字段的数量,当字段很少的时候,Redis内部会采用紧凑编码,节省内存;当字段数量变大或者某个value变大,会自动转换成哈希表结构。这是内部编码机制,我们理解成"小对象省内存,大对象自动切换"就够了。第二是要时刻提醒自己,别把Hash当成"平替String"。如果业务上确实只需要存一个值,用String更直接高效,过度设计Hash反而增加维护成本。
2.3 List类型:从消息队列到排行榜的基石
List类型本质是一个双向链表,或者当你用压缩列表编码时,它也是一个紧凑的双端队列。无论如何理解,它最核心的操作逻辑就是两端进、两端出。LPUSH从左边推入,RPUSH从右边推入,LPOP从左边弹出,RPOP从右边弹出,LRANGE取一段范围内的元素。
很多业务场景用到List,是做简单的消息队列。生产者用LPUSH把消息推入队列,消费者用RPOP取出消息,两边操作互不影响。直接把"消息"理解为一段JSON字符串,整个模型就成立了。这里要注意的是,如果消费者循环空转,RPOP取不到消息的时候会立刻返回nil,白占资源。最佳实践是用BRPOP命令,B表示Blocking阻塞。BRPOP在没有消息的时候会一直挂在那里等待,直到有新消息进来才返回,大大降低了消费者的空转开销。这也是Redis实现延迟通知的基础命令之一。
List的另一个高频场景是时间线或者最新动态列表。比如一个帖子下面有评论,每来一条新评论,你在List的头部推入评论ID,查看评论的时候用LRANGE拿前五十条。这就是一个高性能的"最新评论"实现。需要注意一点:LRANGE取出来的是倒序还是正序,完全取决于你推入的方向。用LPUSH配合LRANGE 0 -1拿到的顺序是最新的在前面,用RPUSH配合LRANGE 0 -1拿到的是最早的在前面。搞清楚推入方向和读取方向,比死记命令参数更重要。
LPOS命令可以查找某个元素在List中的位置,LINSERT能在指定元素前后插入值。这些命令使用频率低,但偶尔遇到需要对列表做定点操作的场景,效果很好。另外,LTRIM能做到只保留指定区间内的元素,比如保留最近100条记录,防止List无限增长。生产环境里如果不做定长控制,List很容易变成"内存黑洞"。
2.4 Set类型:去重、交并集的天然武器
Set是无序且不允许重复元素的集合。它的命令设计非常贴合"集合论"那一套操作。SADD添加元素,SREM移除元素,SMEMBERS取全部元素,SISMEMBER判断某个元素是否存在。SCARD得到集合的基数,也就是元素个数。
Set类型最常用的场景是给用户打标签。一个人可以同时是"会员""老用户""活动参与用户",那你就建一个Set,键叫user:1001:tags,用SADD一次性加进去。以后要判断这个用户有没有某个标签,直接SISMEMBER秒回。要统计这个用户总共有几个标签,用SCARD。这种场景如果用数据库做,要建关联表和索引,在Redis里就是一个命令的事情。
Set真正厉害的地方在集合运算。SINTER求交集,可以找出两个Set共有的元素。比如A注册用户和B注册用户,SINTER能直接算出同时注册了A和B的用户。SDIFF求差集,能算出只属于A不属于B的用户。SUNION求并集,能算出所有注册用户。这些操作不是内存里循环比较,而是Redis在服务端完成,性能极高。像社区系统里"共同好友""可能认识的人"这类功能,用Set命令做,效率和代码量都远胜于应用层循环嵌套。
还有一个不算热门但非常好用的命令:SRANDMEMBER,可以随机从集合里取一个或多个元素,注意它不会删除元素。如果要随机抽取的同时移除,要用SPOP。这两种"取随机值"的命令逻辑完全不同,抽奖系统如果只是发奖名单展示,用SRANDMEMBER;如果奖品发出去就不能再发,必须用SPOP。区分这几个命令,能少踩很多业务逻辑的坑。
2.5 ZSet类型:有序集合是所有排行榜方案的答案
ZSet在Set的基础上给每个元素附加了一个分值,也就是score。集合中的元素按照score从小到大排序。ZADD命令用来添加元素和分值,ZRANGE可以按下标范围取元素,ZREVRANGE则从高到低取。由此,排行榜的需求基本上就是一套固定模板:ZADD实时更新玩家分数,ZREVRANGE取出TOP N,ZSCORE查询某个人的分数,ZRANK查询某人的排名。
ZSet绝大多数坑不在命令本身,而在对分值的设计。score只能是一个双精度浮点数。如果用ZSet存排行榜分数,没问题;但如果你要用ZSet去实现"时间排序",比如把文章的发布时间戳作为score,要留意时间戳位数较大是否超出精度范围,处理不好会出现排名错乱。另外ZINCRBY命令能给某个元素的score增加指定数值,常用于"用户积分变动"这类场景,原子性和性能都很好。
ZSet还有一个隐藏用法,就是用score同时承载两个维度数据。比如把score设计成"总分*1000000+时间戳",使得总分相同时按时间排序。这种设计在竞技排行榜里面很实用。缺点是要对score进行编解码,可读性稍差,但换来的性能提升非常明显。对了,ZRANGEBYSCORE可以用来取一个分数区间内的元素,比如"取分数在80到100之间的用户",做分级筛选的时候非常方便。
3. 键生命周期与内存管理:老生常谈但坑最多的命令区域
3.1 过期时间的正确姿势:EXPIRE、TTL与坑
给Redis键设置过期时间,是几乎所有缓存业务都会做的事情。EXPIRE命令用来给一个已存在的键重新设置过期时间,TTL用来查看还有多少秒过期。但这里藏着一个很隐蔽的坑:EXPIRE只能对已经存在的键设置,如果键不存在,它会返回0表示设置失败。有些人在代码里习惯了SET完再EXPIRE,但中间一旦出差错,键就会变成"永久有效"的脏数据。
我的建议是能用原子方式就用原子方式。刚才提到的SET命令直接带EX参数,或者换成SETEX、SETNX带过期时间的方式,一步到位。如果键是通过其他命令创建的,比如LPUSH创建的List,想设置过期时间,再单独调EXPIRE是没问题的。设置完记得用TTL验证一下,别想当然。PERSIST命令可以移除键的过期时间,让键转成永久,这也是一个很实用的运维命令。
还有一个容易踩坑的地方,是在集群环境下对过期时间的处理。有些同学在单机上用EXPIRE好好的,到了集群环境就发现有的键过期行为看起来不太对。这往往不是Redis的问题,而是因为键可能被哈希到了不同的槽位,或者客户端连接的节点不同。排错的时候先检查自己是不是用了正确的参数形式,比如EXPIRE的第二个参数单位是秒,PERSCOND的第二个参数单位是毫秒。毫秒和秒混用,是很多"诡异过期"问题的根源。
3.2 内存淘汰策略与NOTIFY_EVICTED类型事件
当Redis内存超过maxmemory限制后,会根据maxmemory-policy的配置进行内存淘汰。常见策略有noeviction、allkeys-lru、volatile-lru、allkeys-random、volatile-random、volatile-ttl等。决定用哪种策略不是看文档而是看业务容忍度。缓存场景一般选择allkeys-lru,把最久没访问的键淘汰掉。如果某个键被设置过过期时间,还可以选择volatile-ttl,优先淘汰快要过期的键。
淘汰发生时,Redis会删除键,但这种删除不一定能及时通知到应用层。如果你在业务里用SET带了EX参数,然后设置了过期时间,但内存压力下被提前淘汰,应用层GET那一下会返回空,这是符合缓存预期的。但如果同一个键被用来做分布式锁,提前淘汰可能导致逻辑异常,所以锁场景建议不要依赖volatile类的过期策略,用allkeys-lru的时候也要评估键的重要性。
有一种常用做法是开启key过期事件通知,通过NOTIFY-KEYSPACE-EVENTS配置启用相关事件,然后订阅键失效的消息做补偿逻辑。比如订单超时未支付,可以用SET key value EX 30加订阅过期事件的方式实现。这里命令本身是EXPIRE/SET,但支撑逻辑的是配置和订阅通道。我个人在落地的时候,更倾向把这个方案当成"准实时"系统来处理,不要过度依赖事件通知的及时性,毕竟过期事件的触发时机可能有延迟。
3.3 DEL、UNLINK和异步删除的真正区别
删除命令看似简单,但有一个命令性能差异值得关注。DEL是同步删除,删除大的集合或者大的字符串时,可能会阻塞Redis主线程。UNLINK则采用异步删除,它先把键从键空间里摘除,真正释放内存的工作放到后台线程去做。
我在清理线上Redis大键的时候,几乎只用UNLINK。比如一个List里存了上百万条消息,直接DEL会卡顿,UNLINK能保证删除动作不阻塞。很多团队规范里已经明确规定"删除大键必须用UNLINK",但仍有不少老项目沿用DEL,这是隐患。如果你不确定键的体积,可以用DEBUG OBJECT或者OBJECT ENCODING来查看编码和大小,但这一步在线上要谨慎,DEBUG OBJECT在生产环境尽量少用。简单起见,凡是删除不确定大小的键,我都建议走UNLINK。
4. 线上排障必须掌握的三组命令:有事不慌的基本功
4.1 INFO命令的阅读顺序:从宏观指标到微观定位
线上Redis出问题,第一个动作往往是执行INFO。INFO返回的信息非常庞大,且分成几个段落,正常看的过程中很容易被淹没。我习惯的阅读顺序是:先看Server段的Redis版本、运行时间、进程ID;再看Memory段的used_memory、used_memory_human、mem_fragmentation_ratio,确认内存是否吃紧;最后看Stats段的total_commands_processed、expired_keys、evicted_keys,判断是否有异常淘汰和过期。
很多人看到used_memory快满的时候直接紧张,我却更关注mem_fragmentation_ratio这个指标。如果内存碎片率接近甚至超过1.5,说明碎片严重,可能考虑重启或者用MEMORY PURGE相关手段处理;如果这个值小于1,说明发生了swap,意味着部分Redis内存被换到磁盘,性能会急剧下降。这个判断经验在面试和实战里都很有用,比单纯看内存峰值更能定位问题。
INFO还有一个容易被忽略的Replication段落。主从复制的延迟其实在很多场景里就是一条INFO replication里的master_repl_offset和slave_repl_offset之间差多少的问题。线上如果出现主从数据不一致,先看这两个offset的差值,确认延迟量,再决定是同步等待还是别的策略。这个排查思路不复杂,但很依赖对INFO输出结构的熟悉程度。
4.2 SLOWLOG命令:定位到底是谁拖慢了Redis
SLOWLOG是Redis内置的慢日志系统。配置上有个slowlog-log-slower-than参数,单位是微秒,凡是执行时间超过这个阈值的命令都被记录下来。另一个参数slowlog-max-len决定最多保存多少条日志。线上可以设置成慢日志记录阈值10000微秒,也就是10毫秒。一条命令如果执行了超过10毫秒,在Redis单线程模型下基本意味着批量操作或者大键操作。
查询的时候用SLOWLOG GET,默认返回最近10条,可以用SLOWLOG GET N指定条数。SLOWLOG RESET用来清空历史记录。真正常干的排障操作是:看最近慢日志里出现次数最多的命令是什么,是不是KEYS、HGETALL、LRANGE这类遍历命令,或者MGET、DEL这种大操作。是的话,去应用侧找到对应的调用者,把遍历类操作替换成SCAN或分批处理。定位到命令类型后,用OBJECT命令查一下这个键对应的类型或编码,很多时候能直接看到"bigkey"的影子。
我用SLOWLOG抓到过不少奇奇怪怪的慢命令,比如一个很小的Hash,因为某个字段被HINCRBY不断增加,value变得很大,导致读取整个Hash的速度越来越慢。这种问题不压测根本发现不了,但慢日志会在不断恶化的时候提前给出警告。
4.3 用MONITOR排查实时命令流,但绝不是常规手段
MONITOR命令可以把Redis实时收到的每一条命令打印出来,用于故障复现和异常流量分析非常有效。很多团队会用MONITOR去抓"为什么线上Redis连接数暴涨",看看到底哪些客户端在不断发命令。但MONITOR非常耗性能,每多一个MONITOR客户端,相当于所有的命令执行都要多走一道转储,生产环境的QPS瞬间就会被拖低。所以MONITOR只能在低峰期应急使用,用上一两分钟就必须断开。抓完命令之后,分析命令的pattern,比如看到大量SET在同一秒内执行,再去业务侧对应排查是不是缓存穿透或者缓存击穿。
MONITOR抓到的命令往往是加密连接或者客户端封装的,要还原应用逻辑有点费劲。我通常建议先看慢日志和连接数统计,再决定要不要上MONITOR。毕竟Redis本身就不是一款调试代理,过度依赖MONITOR反而会把性能问题越搞越大。
5. 高级用法与命令组合:从"会命令"到"会方案"
5.1 分布式锁的正确姿势:SET NX EX + LUA
实现分布式锁,很多人第一反应是用SETNX加EXPIRE两步走。但两步操作不具备原子性:如果SETNX成功之后,应用进程在设置过期时间之前挂了,这个锁就永远不会释放,其他线程会一直被阻塞。正确做法是使用一条命令完成加锁与过期设置:SET key value NX EX 30。value要做到每个线程或请求唯一,释放锁的时候要校验value是否还是自己的,再用Lua脚本原子性地比对和删除。
网上有很多现成的Redis锁工具类,但万变不离其宗,核心就是SET NX EX和Lua脚本的DEL操作。理解到这个层面,你再去看任何现成的分布式锁组件,都能看出它的实现思路。需要注意锁的过期时间要结合实际业务耗时设置,设置太短会导致业务没执行完锁就过期,设置太长一旦锁异常,其他线程等待时间太久。更稳妥的做法是采用"看门狗"思路定时延长过期时间,但这种复杂度一般业务不需要。
5.2 Pipeline和MGET:批量操作的正确打开方式
在一个请求里需要读多个键的值,很多人的第一反应是写一个for循环,依次GET。这个做法在键数量少的时候没问题,一旦键数量多起来,比如上百个,每一条GET就是一次客户端与服务端的网络往返,总耗时非常可观。MGET命令能一次传入多个键,批量取回所有value,是这种场景下的首选方案。
但MGET不能无脑用。它适合的是几个键之间没有复杂依赖,单纯批量取值的场景。如果键之间有关联逻辑,比如先判断一个键是否存在再取另一个键,那就没法用MGET,需要自己拆。另一种批量操作是Pipeline,它允许客户端一次发送多个命令到服务端,统一执行,再一次性返回结果。Pipeline适用于一个请求中需要执行多条写命令的场景,比如一次性写入多个键,或者一次INCR多个计数器。
使用Pipeline时要注意,它只负责减少网络往返,并不具备事务性。中间某条命令执行失败,不会影响到其他命令的执行结果。如果业务要求这些命令"要么都成功,要么都失败",需要搭配MULTI/EXEC事务来用。Redis 4.0以后还可以用Lua脚本封装多条命令,保证在一个脚本内的操作是原子的。这三种批量玩法各有适用边界,项目中多用多元组合,尽量不要单一依赖某一种。
5.3 Lua脚本:让多条命令具备原子性
Lua脚本在Redis里的地位很高,官方引入EVAL命令支持在Redis内部执行一段Lua代码。因为Redis在执行脚本时是单线程的,整个脚本会被视为一个不可分割的原子操作。这为"检查再执行"这类复合操作提供了完美方案。
一个常见的例子是"限制用户每分钟只能操作一次"的限流逻辑。你可以用一条INCR加上过期时间实现,但想更复杂一些,比如"每次操作之前判断累计次数是否超过阈值,超过就拒绝",单纯用命令组合做不到原子性,但Lua脚本可以。你在脚本里先GET当前计数,如果超过阈值返回0,否则INCR并设置过期时间,返回1。这样一来,限流判断和计数累加对客户端来说就是一个整体,不会出现多个客户端同时绕过限制的情况。
Lua脚本也有需要注意的地方:脚本内不能依赖和外部交互,比如获取当前系统时间就可能和Redis server端时间不一致;脚本执行期间会阻塞其他命令,脚本越短越好,越简单越安全。实际使用中我还发现,Redis集群模式下Lua脚本要求访问的键必须在同一个哈希槽。否则会报CROSSSLOT错误。做一个限流键的时候,一般用相同前缀加固定后缀保证落到同一槽位,问题不大。多键操作的场景就要先考虑槽位分布,设计不好脚本就没法运行。
5.4 发布订阅:命令之外的轻量消息机制
除了Stream和List队列之外,Redis还提供了Pub/Sub消息模型。PUBLISH命令用于发布消息,SUBSCRIBE命令用于订阅一个频道。很多人忽略这个功能,但它确实适合一些不需要消息持久化、不需要累积消息的广播场景,例如系统通知、配置刷新、节点上下线提醒。相对消息队列组件,Pub/Sub具备"即发即收"的特点,如果业务可以接受丢消息,那它是最轻量的方案。
用Pub/Sub要特别留意:消息不会被Redis持久化。如果消费者恰好不在线,消息就丢了。另外,PUBLISH返回的是收到消息的订阅者数量,不是消息本身是否成功入库。某些业务希望"至少一次"的投递语义,那Pub/Sub就不合适。更重要的是,SUBSCRIBE操作会进入"订阅模式",这个连接不能再执行普通的键命令。一些客户端框架会建立专门的订阅连接,避免影响正常的操作。
5.5 Stream类型:有序消息的现代选择
如果你确实需要消息队列的语义,最好使用Redis 5.0引入的Stream类型。XADD添加消息,XREAD读取消息,XGROUP创建消费组,XACK确认消息。Stream能做到消息持久化,支持消费者组,能挂多个消费者协同消费,还支持消息ack机制,这比List队列和Pub/Sub都更有保障。
我一直认为Stream是Redis作为"消息中间件"的重要进化。它不需要额外的磁盘存储,完全借助Redis自身的内存和RDB/AOF持久化能力。它也有自己的复杂性,比如消费组的创建时机、消费者的offset管理、消息积压时会占内存。对消息可靠性要求很高的核心环节,我仍然建议上专门的消息组件;如果只是做一些内部异步任务和事件总线,Stream是一个性价比相当高的选择。
6. 实战排查案例:一次由HGETALL引发的Redis阻塞复盘
线上环境曾遇到一个很诡异的间歇性卡顿。服务整体CPU不高,网络也没明显波动,但某些业务接口的P99时延会突然跳到上秒。第一轮排查看Redis的INFO,发现connected_clients没有异常涨跌,但瞬间命令处理量出现明显的"掉坑",好像在某一个极短时间段内Redis被"冻住"了。
我随后用SLOWLOG GET查慢日志,发现大量HGETALL命令耗时异常。但我当时的第一反应是"不可能",因为平时看到的Hash都很小。直到我顺着业务埋点找到那个key,发现它在运行过程中被不断写入新字段,字段数从初始的几十个一直膨胀到上万级别,单个Hash对象体积已经非常可观。HGETALL看起来是"取对象全部字段"的常规操作,但在大Hash面前,它会占用大量内存并持续阻塞主线程。
问题的根因不是HGETALL本身,而是业务侧设计失误:用一个Hash堆积了一整个月份的操作记录。最终修复方案很朴素:把这个大Hash按天拆分,每天一个key,并且对历史key设置过期时间,同时把应用里"每次全量取"改成"只取最近N天字段"的批量读取。上线后慢日志清零,P99时延恢复到正常水位。复盘这个case的意义在于,很多Redis性能问题不是命令本身慢,而是命令和数据规模组合之后变慢了。排查时不能只盯着"命令类型",要把键的大小和命令执行频率结合起来分析。
7. 最后再分享几个"命令执行之前"的实用习惯
无论是初学者还是老兵,养成一些前置检查习惯,胜过背一百条命令。第一,执行潜在的批量命令之前,先想清楚这个键有可能已经膨胀到多大。可以用DEBUG OBJECT或OBJECT ENCODING看编码情况,但更要结合业务模型去估算数量级。第二,涉及删除或者覆盖的重要键,先在从节点或者测试环境验证命令参数,不要拿线上数据试错。UNLINK和DEL的参数形式一样,但语义完全不同,敲错了一个键可能就是不可恢复的损失。第三,执行EXPIRE之前,先用TTL确认当前键的过期时间,很多东西不是你设置的"永久键"就真的是永久键。第四,搞清楚自己连接的Redis是不是集群模式,集群模式下多键命令的限制比单机多得多,MGET和DEL都不支持跨槽位操作,这个问题会直接在你的业务代码里报错。
Redis命令本身不难,难的是在正确的时间、正确的数据形态下选择正确的命令,并且知道每条命令背后的开销边界。我接触Redis的时间越长,越觉得"命令详解"这四个字的重点从来不在"语法解释",而在"场景落地"和"异常处置"。希望这边梳理过的内容,能帮你在实际项目中少走几步弯路,至少在遇到Redis性能问题的时候,脑子里能快速浮现该用哪几条命令去定位和止损。