凌晨三点微信群里的告警机器人突然开始刷屏redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool。你揉着眼睛打开电脑连接数打满CPU 100%INFO一查keys一个hotkey的ttl直接过期然后所有请求像饿狼一样扑向了数据库——这一个晚上Redis 这个“最快”的中间件成了整个系统里最慢、最脆的环节。Redis 看着简单无非get/set/del加一堆数据结构但真正让它成为“黑暗森林”的不是那些面经里的八股题而是藏在命令、配置和业务节奏里的一组隐形陷阱。我这些年处理过的线上事故十有八九是夜里由这些陷阱引爆的。下面这 10 个是我认为最值得刻在脑子里的按杀伤力和隐蔽程度排了个序每一个都配上排查命令和避坑操作务必收藏。1. 大 Key 删除阻塞主线程的“慢动作炸弹”1.1 大 Key 是怎么悄悄长出来的很多人对“大 Key”没有概念总觉得 Redis 嘛内存里几百 M 的数据很正常。但这里的关键不是内存大小而是单个 Key 内部结构的大小。比如一个list里存了几百万条消息一个hash里塞了几十万个字段甚至一个普通的string值有几 MB 的 JSON。这些 Key 平时读写没问题一旦你去DEL它就出事了。为什么Redis 是单线程处理命令的这里不谈多线程 IO 和异步删除的细节执行DEL的时候如果这个 Key 是个大集合它需要遍历所有元素去释放内存。这个遍历过程在事件循环里是同步的期间其他所有命令都堵在门口排队。等你好不容易把DEL跑完了客户端这边的超时错误已经刷了好几屏了。我见过最夸张的一个案例一个hash存了半年的用户行为数据大概 200 万个 field某天做清理任务时直接DEL掉主线程阻塞了整整 8 秒。这 8 秒里所有缓存读写全部卡死上游服务超时链路雪崩。而且这类问题很阴的一点是平时你看 CPU 不高、内存也没满根本想不起来去查它。1.2 发现与安全删除大 Key 的实操方法发现手段很简单Redis 自带了扫描工具redis-cli --bigkeys它会遍历整个实例生产环境找个低峰期跑按string/list/set/hash/zset分类找出最大的 Key 和大小排名。还有一个更精细的排查方式用DEBUG OBJECT key看序列化长度redis-cli DEBUG OBJECT user:behavior:202401返回里的serializedlength能告诉你这个 Key 占用多少字节。如果发现单个 Key 超过 100KBstring类型或者集合元素超过 1 万个我建议就当成大 Key 来治理了。删除时千万别直接DEL。比较新的 Redis4.0 以上提供了UNLINK命令它是异步删除主线程只负责把 Key 从字典里摘掉立刻返回真正释放内存的工作交给后台线程redis-cli UNLINK user:behavior:202401如果手头还是老版本或者面对的是redis-cluster里某些受限场景那就用分批淘汰的办法HSCAN配合HDEL每次删 100 个 field循环几次删完把阻塞时间切成碎片而不是一次性砸下去。建议新写代码时任何集合类型的 Key 都要预设一个合理的“拆除方案”。要么设过期时间自动消失要么写清除脚本时统一走UNLINK。永远不要把DEL用在没把握的大 Key 上。2. 过期时间的“薛定谔状态”不太会儿就彻底消失2.1 永远不过期的 Key 是怎么把内存吃光的很多开发为了图省事存缓存不设TTL美其名曰“反正数据量不大”。然后半年后你就会看到INFO memory里used_memory一路走高maxmemory设了等于没设——因为淘汰策略还没触发内存先满了。最坑的不是内存增长本身而是没有过期时间 这个 Key 永远得不到清理机会你既不知道它还剩多少空间也不知道它哪天把实例撑爆。我接手过一套老系统缓存 Key 有几十万个从第一个到最后一个全是永久 Key根本没法清理最后只能挑低峰期写脚本按前缀批量扫出来加过期时间。2.2 过期机制的三个隐藏细节第一个细节惰性删除 定期删除。Redis 不会实时盯着你的 Key 等它过期它靠的是定期随机抽查一部分过期 Key 删除外加每次访问时检查当前 Key 是否过期。这意味着一个已经到期的 Key如果没人访问它可能还会在内存里躺很久最多到定期删除扫描到它。第二个细节过期 Key 在 master 和 slave 上的表现不一致。在 Redis 集群主从架构下过期删除是由 master 发DEL命令同步给 slave 的slave 自己不做主动过期。如果 master 挂了slave 还没来得及处理DEL同步就升为 master那理论上应该消失的 Key 会“短暂复活”而且新 master 上它的 TTL 可能变了。这就是分布式锁场景里经典的“锁提前失效/永久不失效”问题来源之一。第三个细节别把EXPIRE设置当成一劳永逸。业务更新了同一个 Key 并重新SETTTL 会被清掉。很多人改完数据忘了重设过期时间结果 Key 就变成了永久 Key内存压力就是这么一天天堆起来的。写代码时我一般建议用统一的缓存工具类封装SETEX、SET key value EX seconds这类带过期时间的原子命令是默认选项。3. 分布式锁主从切换那一刻的“锁丢失”3.1 简单 SETNX 锁为什么会在极端场景下失效网上有一堆 Redis 分布式锁教程最经典的实现是SET lock:order unique_value NX EX 30。这套逻辑在单实例下没问题但在主从架构或哨兵架构下有个著名的坑如果锁写入 mastermaster 还没来得及把这条数据同步给 slave 就宕机了哨兵把 slave 提升为新的 master其他线程就能对新 master 再次加同一把锁。旧持有者还在跑业务新持有者也进来了互斥直接失效。这个问题的严重程度取决于你的业务对互斥的容忍度。如果只是防止缓存击穿时重复查库问题不大如果拿它做金融级别的幂等控制那就是事故级别。还有一种更常见的锁失效场景锁过期时间设置太短业务执行没跑完锁先没了。我见过真实案例一个定时任务平均执行时间 20 秒锁过期时间设了 10 秒结果两个节点同时执行产生了双份数据。排查日志时崩溃了代码逻辑完全正确但就是脏数据。3.2 加锁过程的完整校验与 Lua 原子性正确姿势至少要做三步第一步加锁时用唯一随机值作为value。加锁命令应该是SET lock:order b3f0xyz123 NX EX 30第二步释放锁时不能无条件DEL。要用 Lua 脚本先比对 value 再删除防止自己超时后别人加的锁被自己误删if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end第三步对执行时间不可控的业务要么给锁加一个“看门狗”续期机制这也是 Redisson 里watchdog的实现思路要么接受“锁过期 业务超时”的极端场景在业务侧做幂等兜底。我个人实践中更推荐“缩短业务执行时间 精确设置 TTL 兜底幂等校验”三个一起上完全指望续期机制反而把系统搞复杂。经验分布式锁在 Redis 里不是“银弹”。能用数据库唯一索引解决的互斥就别上锁必须上锁的一定要识别出“锁提前失效”的业务后果是什么能不能补偿。4. 缓存穿透、击穿与雪崩三兄弟一个比一个狠4.1 穿透查了一个根本不存在的 Key缓存穿透就是大量请求查询一个缓存里没有、数据库里也不存在的数据。比如恶意用户疯狂用不存在的 ID 打接口缓存永远查不到请求全部落到数据库数据库连接被打满。最经典的解决方案是三层第一层参数校验非法 ID 直接拒绝第二层缓存空值比如查不到商品就缓存一个“空标记”30 秒告诉下游这个 ID 没有商品第三层布隆过滤器启动时把所有合法商品 ID 全部 hash 到一个 bitmap 里请求来了先过布隆查不到的直接返回不过数据库。布隆过滤器有误判率但它只会误判“存在”而不会误判“不存在”所以不会把真实数据挡掉。我个人的建议是空值缓存是最省事且见效最快的布隆过滤器适合数据量特别大、ID 连续性不明显的系统。不要一上来就追求过滤器先看业务规模和异常请求量。4.2 击穿与雪崩热 Key 过期的连锁反应缓存击穿是指某个特别热的 Key 正好在过期的那一瞬间来了大量请求缓存没挡住全打到数据库。雪崩则是大量 Key 在同一时间段集体过期数据库瞬间扛不住。前者是单点热 Key后者是群体性过期。解决击穿最简单的方式是“互斥锁重建缓存”发现缓存过期后先让一个线程去查库重建缓存其他线程暂时阻塞等待。重建完以后大家都走缓存数据库压力只有一次查询。解决雪崩最简单的办法有两个第一个是过期时间加随机数比如 TTL 设为 5 分钟加 0 到 60 秒的随机值避免整片 Key 同步死亡第二个是热点数据永不过期 后台异步刷新访问时发现数据快过期就主动去刷新缓存这适合读多写少的高热场景。这里有一个容易忽略的细节如果用了“互斥锁重建缓存”重建期间其他线程不能真的无限阻塞要设置一个等待超时超时后放行去查库然后承受一次小规模的并发击穿保全整体可用性。我在实际项目里的做法是等待 200ms拿不到锁就强制查库返回同时记录告警日志靠“短暂击穿一次 监控告警”换“不长时间阻塞”。5. key 序列化你看得懂的代码Redis 读不懂5.1 序列化混用明明都是 Redis数据却是“天书”很多团队的 Redis 缓存会同时被 Java、Go、Python 多个服务读写。Java 里默认的JdkSerializationRedisSerializer会把对象序列化成一坨以\xAC\xED\x00\x05开头的二进制数据里面包含了类名、包名、序列化版本号。这玩意儿 Java 自己反序列化没问题但 Go 的服务去读直接解析失败。更可怕的是同一个 Key 如果先被 Go 写成了 JSON 格式Java 再去读反序列化直接报错。这个问题的隐蔽性在于单服务开发时一切正常一旦跨语言协作或者切换了序列化框架线上数据就全乱套了。排查的时候你会看到缓存数据变成了一堆乱码完全不像 Redis 该有的样子甚至敲命令都看不出问题。5.2 统一序列化方案与版本兼容策略我的建议是团队内部约定统一的序列化协议默认用 JSON。Java 侧用GenericJackson2JsonRedisSerializer带上class类型信息Go 侧就用标准的encoding/json。如果非要存对象就存合理粒度的 DTO不要直接把实体类怼进缓存这样既减少冗余字段也降低反序列化失败的风险。对于已经存在的“历史遗留序列化混乱”数据不要试图读出来转格式直接让缓存失效重建。给 Key 加个新前缀比如v2:让新旧数据自然过渡比在线上写“数据转换脚本”安全十倍。顺带提醒一个细节反序列化失败不等于 Redis 不可用。很多时候你的服务日志里报Could not read JSON但 Redis 本身响应正常问题出在代码里的RedisTemplate配置上。排查时先看一眼缓存内容长什么样再决定是清缓存还是改配置。6. RDB 与 AOF 持久化夜里 fork 一下主线程就“抖”一下6.1bgsave与fork的噩梦账单Redis 的持久化默认是 RDB 快照触发bgsave时需要fork一个子进程来拷贝内存页表。fork过程本身在 Linux 上是“写时复制”的理论上不会真正拷贝全量内存但它有个躲不开的开销fork 的那一瞬间操作系统要扫描父进程的全部内存页表内存越大这个耗时越长。一个 30GB 的 Redis 实例fork可能阻塞 300 毫秒甚至 1 秒。别小看这一秒。对高 QPS 的业务主线程阻塞 1 秒的含义是所有请求排队 1 秒超时一大片。更惨的是如果业务触发了一次手动SAVE同步持久化那阻塞时间直接翻倍堪称事故级操作。6.2 持久化策略怎么配才不会半夜炸生产环境我的配置原则是三个字能不要就不要。如果 Redis 纯粹做缓存挂了可以重建我建议彻底关闭 RDB 和 AOF把save 写进配置内存靠缓存重建机制自愈。这能省掉 fork 阻塞、磁盘 IO 两个大坑。如果必须做持久化比如做 session 存储或临时数据存储那就只开 AOF并且选appendfsync everysec。这个配置每秒钟把写命令刷一次磁盘最多丢 1 秒数据性能影响可控。不要开always那等于每个写命令都 fsyncRedis 性能直接腰斩。RDB 和 AOF 同时开着不是不可以但你要理解它们各自会在什么时候触发写盘低峰期还好高峰期一旦同时碰上bgsave 业务写入高峰IO 延迟告警几乎是必然的。我见过最惊险的运维操作是在晚上流量高峰期手动敲了个BGSAVE然后整个集群的延迟从 2ms 飙到 50ms。记住这条铁律线上不要在高峰期做任何手动持久化操作。排查 fork 阻塞可以用redis-cli INFO stats | grep latest_fork_usec这个值如果超过 100 万微秒即 1 秒那你这个实例的内存规模对单机来说已经偏大了考虑拆分为 Redis Cluster 或者用多个小实例分摊内存。7. 慢查询不是 Redis 慢是命令太“重”7.1KEYS *就是一把“全表扫描”的冲锋枪KEYS *这个命令你在本地两万 Key 的环境跑没感觉在线上全量几百万 Key 的实例上跑一次直接阻塞主线程几百毫秒。它就是数据库里的SELECT * FROM table而且是没加 LIMIT 的那种。很多人会问那我想遍历 Key 怎么办答案是SCAN。它每次返回一小批 Key默认 10 个配合游标游走不会阻塞主线程。但注意SCAN的循环条件不能想当然你要用一个循环去取完整游标redis-cli --scan --pattern user:* --count 100--count可以调大但别太贪100 到 1000 之间比较合适太大照样会引起瞬时延迟。7.2 慢查询日志怎么用起来Redis 自带慢查询日志两个配置要打开slowlog-log-slower-than微秒阈值默认 10000 微秒 10ms和slowlog-max-len保留多少条日志。我一般会把阈值调到 5000 微秒5ms抓更细的慢命令。排查慢命令用redis-cli SLOWLOG GET 10它会把最近 10 条慢命令原样打出来包括命令参数、耗时、时间戳。看到耗时长的命令再针对优化如果是MGET打不过来的批量请求拆成小的批次如果是ZRANGEBYSCORE干了一大批排序看能不能用别的结构替代。隐藏的坑在SMEMBERS这类命令取整个 set 的所有成员。平时只有几十个元素没问题如果 set 长了这个命令耗时是 O(N)线上很容易直接被一个脚本拉满。更安全的替代方案是SSCAN分批拿。8. 热点 Key 与连接管理被流量冲垮的“单点瓶颈”8.1 热 Key 把单节点 CPU 打到 100%热点 Key 问题是 Redis 架构里最难根治的问题之一。某个商品秒杀、某个热搜话题、某个明星动态一瞬间同一 Key 的 QPS 暴涨Redis 单实例的 CPU 被打满所有读写延迟飙升。常规解法有几个方向第一本地缓存多级缓存。在应用内存里缓存一份热点数据设置很短的过期时间比如 1 秒命中后根本不查 Redis。比如 Java 侧用 CaffeineGo 侧直接用 sync.Map 存一下把热点流量挡在应用层。这个方案最有效副作用是每台机器会短暂持有旧数据但热点场景通常能接受秒级延迟。第二热 Key 的复制 随机后缀。把hotkey复制成hotkey:1、hotkey:2、hotkey:3写的时候同步写多份读的时候随机挑一个 suffix。这样流量分散到多个 Key 上垂直打散单 Key 的压力。缺点是需要业务代码配合且数据一致性要自己保证。第三监控与告警。在没有出现故障前就要有盯着热 Key 的手段。redis-cli --hotkeys可以扫出访问频率最高的 Key配合监控平台看CPU和keyspace_hits。8.2 连接池满Redis command timed out的真相线上最常见的报错就是这句Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException。很多人第一反应是“给连接池加连接数”但大多数情况下真正的原因是连接池被打满 有慢命令阻塞了连接释放或者服务端已经不堪重负请求排队超时。排查思路不能盲目第一步看服务端redis-cli INFO stats里的connected_clients和blocked_clients如果connected_clients异常高说明有连接泄漏的风险。INFO commandstats看高频命令如果某个命令的调用次数特别大可能就是它把连接占住了。第二步看客户端侧检查连接池配置。Lettuce 默认超时是 10 秒如果你用的是Jedis则看timeout参数和maxTotal。我遇到过一个经典场景maxTotal设成 500单机 20 个实例峰值流量上来每实例需要 30 个连接理论上够用但某个业务查询在 Redis 上耗了 3 秒连接被占用其他查询就只能等超时。把连接池上限调大只是延缓问题真正的解法是找到那个 3 秒的慢命令。9. 连接与资源泄漏每个“疑难杂症”背后都有一个关闭失败9.1 忘了close()的客户端连接池泄漏Java 用 Jedis 的时候经常有人手写jedis.getResource()用完了却忘记close()。这里的close不是真正的 TCP 关闭而是把连接归还给连接池。如果你不归还连接池的可用连接越来越少最终全部耗尽。排查方法是看服务端INFO的连接数connected_clients超过几百并且持续增长不下降基本可以断定有连接泄漏。逐个服务排查重点看线程池里的请求是不是“吃完不吐”。Go 和 Python 也有类似问题Go 的redis.Client是连接池管理的一般不用手动释放但从连接池Get出来的redis.Conn必须要Close否则连接数照样涨。9.2 无界队列与背压超时的根本不是 Redis另一个容易忽视的点是你的服务在 Redis 变慢时自身也会跟着变慢然后线程池队列堆满。Redis 的 timeout 报错只是表象真正的元凶是服务端线程池满了、任务在排队。处理方式就一句话给 Redis 调用加上“有界信号量 快速失败”。每个业务接口的 Redis 调用加上合理的超时和并发限制Redis 慢的时候宁可快速返回失败也不要让全部线程趴在 Redis 上等。这也是后端服务设计里的背压思想不让一个环节的抖动拖垮整条链路。10. 可视化客户端的“隐形手雷”你怎么死的都不知道10.1 刚打开 Redis Desktop Manager顺手一删Redis Desktop Manager 和 Another Redis Desktop Manager 这类可视化工具极大地方便了调试但也带来了一些意料之外的问题。最经典的场景是线上排查问题时你想看看某个 Key 的值于是打开工具连上生产 Redis输了个KEYS *。这个操作在可视化工具里只是一个按钮但工具后台执行的就是KEYS *全量实例直接被拖慢。另外一个坑是可视化工具默认连接是走的普通 TCP不代表生产环境就能直连。生产环境的 Redis 一定要设置访问控制即便在内网也不应该让每个开发者都能直接连上去查数据。我之前就遇到过一个同事用可视化工具连上生产环境清理“疑似无用”的 Key一晚上删掉了三套业务正在用的数据第二天业务全废了。10.2 可视化工具的正确使用姿势我对可视化客户端的建议是调试环境随便用生产环境只能用只读账号。Redis 6.0 以上支持 ACL可以创建一个只读用户限制只能执行GET、MGET、HGETALL等只读命令没有DEL、FLUSHALL、KEYS的权限redis-cli -a password ACL SETUSER readonlyuser on pwd ~readonly read connection生产环境连接工具的账号必须配成这个只读用户从工具层面断绝误操作的可能。另外排查生产数据尽量用redis-cli命令行配合SCAN而不是KEYS并且把它放在跳板机上操作不要从你本地 Mac 直接连生产 Redis。写在最后这些“隐形陷阱”里最讽刺的是每一个在事发前都显得完全“正常”。大 Key 平时读写没感觉慢查询日志没去看就一直没人知道连接池数字只要不到 100% 就没人关心。Redis 的黑暗森林其实就是“默认配置 失控增长 缺少监控”这三件事的合谋。我个人处理事故时的习惯是先把INFO、SLOWLOG、MONITOR三个工具抓起来再看业务代码最后才碰配置。因为大部分 Redis 事故都不是 Redis 本身坏了而是你敲给它的命令、存进去的数据、设好的过期时间在某个特定流量形态下恰好变成了炸药。这里没有放之四海皆准的“最佳配置”只有一套结合业务特点的持续治理预案。希望你读到这里时能想起你自己的 Redis 里是不是也藏着某个让你失眠的隐形定时炸弹。
阅读完成 · 觉得有帮助?