QPS翻倍后Redis崩了?这3个缓存策略救了我一命

早些年做架构的时候,我曾天真地以为,只要给数据库加上一层 Redis,再给每个 Key 设个随机过期时间,就能搞定 90% 的高并发问题。

直到两年前的一次双十一大促预演,现实狠狠给了我一巴掌。

那天下午2点,流量只是比平时翻了一倍,我们的数据库 CPU 却突然飙到了 100%,报警群里的消息像机关枪一样响个不停。排查下来发现,不是 Redis 挂了,而是缓存策略太粗糙——几个热点 Key 同时过期,加上一个看似不起眼的 Big Key,直接击穿了数据库,顺带把带宽打满了。

这次“踩坑”让我深刻意识到:在中小团队资源有限的情况下,盲目堆机器没用,精细化的缓存策略才是保命符。

今天想和大家复盘一下,我在那次事故后总结的一套适合中小项目的 Redis 避坑指南,不讲虚的理论,只聊怎么落地。

## 策略一:热点 Key 别用强一致,试试“逻辑过期”

很多兄弟在处理热点数据(比如首页 Banner、爆款商品详情)时,最怕的就是缓存击穿

配图

经典的教科书方案是“加互斥锁(Mutex Lock)”:当缓存失效时,只允许一个线程去查库重建缓存,其他线程等待。

说实话,这在理论上很完美,但在我们那次事故中,因为代码里的锁粒度没控制好,加上数据库响应慢,导致大量线程阻塞在应用层,Tomcat 线程池瞬间被打满,整个服务直接假死。

后来我改用了“逻辑过期”方案,效果出奇的好。

简单说,就是物理上不给 Key 设置过期时间(也就是永不过期),而是把过期时间戳写在 Value 的内容里。

{
  "data": { "title": "iPhone 15", "price": 5999 },
  "expire_at": 1715682000000  // 逻辑过期时间
}

落地逻辑是这样的:

  1. 应用拿到数据,先判断 expire_at 是否过期。
  2. 如果没过期,直接返回数据。
  3. 如果已过期,先返回旧数据给用户(这一步是关键,用户对几秒的数据延迟通常不敏感),同时异步启动一个线程去数据库查新数据,更新缓存。

这个方案最大的好处是服务永远可用,不用担心大量线程阻塞。哪怕数据库挂了,用户至少还能看到旧数据,而不是 404 或转圈圈。对于追求高可用的中小团队来说,这比追求绝对强一致性划算得多。

## 策略二:消灭 Big Key,比扩容更重要

那次事故中,另一个罪魁祸首是一个“用户详情”的 Key。

开发这个功能的兄弟为了图省事,把用户的基本信息、最近 10 条订单、最近 50 条浏览记录全塞进了一个 Key 里。平时测试数据少没感觉,大促时某些活跃用户的 Value 居然膨胀到了 500KB。

你可以算一笔账:如果有 1000 个并发请求同时读取这个 Key,光网卡带宽就占了 500MB/s(4Gbps)。我们当时的云服务器网卡直接瓶颈,导致 Redis 响应变慢,拖垮了整个链路。

我现在的复盘经验是:

也就是我常挂在嘴边的“极简原则”——Redis 是缓存,不是数据库,别什么垃圾都往里塞。

针对 Big Key,我有两个非常硬性的落地手段:

  1. 拆分(Split): 把那个巨大的“用户详情”拆成 user:basic:101(基础信息)、user:orders:101(订单列表)。前端页面需要哪里取哪里,别搞“全家桶”。
  2. 压缩与裁剪: 对于不得不存的长文本,必须用 Snappy 或 Gzip 压缩;对于列表,设置严格的长度限制(比如只存最近 10 条),超出的部分去查库。

我现在每周五下午做 Code Review 时,都会专门让团队扫一遍 Redis 里的 Key 大小,超过 10KB 的必须说明理由,这已经成了我们的铁律。

## 策略三:缓存一致性,放弃“既要又要”

“先更新数据库,还是先删除缓存?”这个问题,在面试里能聊半小时,但在实战中,中小团队最容易被绕晕。

配图

我们也尝试过复杂的“延迟双删”策略,甚至想引入 Canal 监听 Binlog 来异步更新缓存。结果呢?架构变得极其复杂,Canal 偶尔挂一次,排查问题能要了半条命。

对于大多数并发量在万级(QPS < 10k)的业务,我强烈建议采用 Cache Aside Pattern(旁路缓存模式)的改良版,别搞太复杂。

我的实操建议:

  1. 读数据: 缓存有就读,没有就查库并回写。
  2. 写数据: 先更新数据库,然后直接删除缓存(注意,是删除,不是更新)。
  3. 兜底: 给所有缓存必须加上一个较短的 TTL(过期时间),比如 5 分钟。

为什么这么做?因为“删除缓存”的操作是幂等的,比“更新缓存”出错概率低。而加上 TTL,是为了保证最终一致性——哪怕因为网络抖动导致删除缓存失败了,5 分钟后缓存自动过期,数据也就修正过来了。

对于中小团队,“短 TTL + 失败重试” 的性价比,远远高于引入一套庞大的消息队列或中间件来保证强一致性。业务能接受 5 分钟的不一致,你就没必要为了那 0.01% 的完美去增加 50% 的架构复杂度。

## 总结与行动

做架构设计,最忌讳的就是手里拿着锤子,看什么都是钉子。Redis 确实快,但它不是无限容量、无限带宽的神器。

如果在高并发场景下,你只能做三件事来提升 Redis 的稳定性,我建议是:

  1. 扫盲: 用工具(如 redis-rdb-tools)把线上大于 10KB 的 Key 找出来,要么拆,要么删,这是立竿见影的性能提升。
  2. 隔离: 别让所有业务共用一个 Redis 实例。把核心业务(如交易)和边缘业务(如日志、非核心计数)物理隔离,几百块钱的成本能省去几万块的故障损失。
  3. 降级: 永远要有“如果 Redis 全挂了,系统能不能跑”的预案。即使是只能提供静态页面,也比白屏强。

最后,做个小调查:

在处理热点数据缓存失效时,你更倾向于用“互斥锁”来保证数据准确,还是用“逻辑过期”来保证服务可用? A 还是 B?

配图

欢迎在评论区告诉我你的选择和踩坑经历。


本周行动建议: 如果你现在的项目里也有“一坨”大的 JSON 存在 Redis 里,明天上班第一件事,把它拆了。相信我,你的网卡会感谢你的。