从数据模型到命令语义:Redis实战排障指南
很多人学Redis习惯打开官方命令手册一条一条去试今天试个SET明天试个LPUSH。看的时候感觉全看懂了真到线上出问题脑子里翻遍命令文档也想不起该用哪条命令救火。这种学法不是不对是太低效。我做了这么多年的后端开发和系统维护Redis命令前前后后摸过上百条真正在项目里高频使用的其实就是一个很小的子集。这篇文章不打算把所有命令罗列一遍那没有意义我想换个拆法从数据模型讲到命令语义从命令语义讲到实操排障把我自己趟过的坑和积累下来的经验一并说透。先说一个很多人容易忽略的底层认知Redis的命令不是随随便便拼出来的每一条命令的设计都对应着一种数据结构的操作方式和业务场景。你理解了String、Hash、List、Set、ZSet这些底层数据模型之后再看命令就会发现它们不是零散的一百多个动词而是一套有序的数据结构操作接口。写代码的时候你可能感受不到这种区别但等你排查线上问题需要在不看文档的情况下精准敲出正确命令这种认知就变得极其关键。1. Redis命令的分类框架先建立自己的认知地图1.1 两类维度看命令按操作对象和按能力范围学Redis命令之前我建议你先做一个分类不要一上来就记KILL、CONFIG这种零散的运维命令。第一类维度是按操作对象也就是数据类型来分这对应你去操作某个键的值。比如String类型的SET、GET、INCRHash类型的HSET、HGET、HGETALLList类型的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 NZSCORE查询某个人的分数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命令定位到底是谁拖慢了RedisSLOWLOG是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性能问题的时候脑子里能快速浮现该用哪几条命令去定位和止损。