活动刚开始的前十分钟,商品详情页先慢了。
下单接口还没到峰值,支付也没出问题,但数据库 CPU 已经开始往上冲。排查后发现,大量请求都在查同一个商品详情、活动配置和库存展示。每个请求都不复杂,可它们太密集了,密集到数据库开始喘不过气。
这种场景在监控上看得很直观。
业务群里先有人说“页面有点慢”,运维那边很快甩了一张图:数据库 CPU 往上贴,连接数也在涨。开发第一反应通常是看慢 SQL,结果发现单条 SQL 并不算离谱,就是同一批查询被高频打了太多次。
这时候你会意识到,问题不一定是某条 SQL 写烂了,而是系统把不该重复问数据库的问题,一遍又一遍交给了数据库。
这时候加缓存,不是为了让页面快一点那么简单。
缓存真正要做的,是替数据库挡住那些没必要每次都落到数据库上的读请求。它保护的不是某一行代码,而是系统里最贵、最脆弱、最不该被高频读拖垮的那一层。
真正要先看清楚的,不是要不要上 Redis,而是哪一类请求本来就不该每次都问数据库。
一、缓存不是加速器,首先是缓冲层
一个商品详情,如果每次访问都查数据库,小流量时没问题。
但活动流量一上来,问题会被放大:连接池被占满,慢查询堆积,锁等待变长,数据库还要同时处理下单、库存、支付回调这些更关键的写入。
缓存的第一层价值,是把高频读请求挡在数据库外面。
商品详情、首页配置、活动规则、热门排行榜,这些数据不是每秒都变,却可能每秒被读很多次。它们适合放到缓存里,让数据库只在缓存缺失、数据更新或缓存重建时参与。
所以我不太喜欢把缓存只理解成“让系统变快”。快只是表象。更准确地说,缓存是在给后端系统留呼吸空间。
二、缓存穿透:不存在的数据也会打穿系统
活动系统上线后,经常会遇到一些奇怪请求:不存在的商品 ID、不合法的订单号、被删除的活动页。
如果这些请求每次都先查缓存,缓存没有,再查数据库,数据库也没有,最后什么都不缓存,那下一次同样的请求还会继续打到数据库。
这就是缓存穿透。
这种请求不一定都是恶意攻击。
有时候是前端参数没处理好,有时候是爬虫扫 URL,有时候是用户刷新了一个已经下架的活动页。更麻烦的是,它们单次看起来都很轻,不像明显的异常流量,所以很容易被忽略。
处理它,我一般分三层。
第一层是参数校验。明显不合法的 ID、异常分页、过大时间范围,应该在入口直接拦掉。
第二层是空值缓存。对于格式合法但数据不存在的请求,可以短时间缓存空结果,比如 30 秒到 5 分钟。
第三层是布隆过滤器。对于商品 ID、用户 ID 这类容易被批量探测的公开接口,可以先判断“这个 ID 有没有可能存在”,再决定是否进入缓存和数据库。
这里有个细节:空值缓存时间不能太长。否则刚创建出来的新商品,可能会被之前的空缓存挡住。
所以空值缓存不是“缓存一个不存在就完事了”。它也要看业务更新频率。如果商品、活动、设备这类数据随时可能新建,空值 TTL 就要短一点;如果是历史订单号这种基本不会后补的数据,时间可以稍微放宽。
三、缓存击穿:最怕热点 Key 刚好过期
缓存击穿访问的是一个真实存在的数据,只是它刚好是热点,又刚好在高并发时过期了。
比如活动页主推商品,平时请求都命中 Redis。某一秒 Key 过期,几千个请求同时发现缓存没了,于是一起查数据库。数据库原本只需要处理一次查询,现在被迫处理一波并发查询。
这个问题的核心,不是缓存没用,而是缓存重建没有保护。
普通热点 Key 可以用互斥锁,只允许一个请求查数据库并重建缓存,其他请求短暂等待后重试。
如果访问量特别高,我更倾向于逻辑过期:缓存物理上不过期,只在 value 里放逻辑过期时间。请求发现逻辑过期时,先返回旧数据,再异步刷新。
代价也要说清楚:逻辑过期会让用户短时间看到旧数据。所以它适合商品详情、活动配置、排行榜,不适合余额、支付状态、库存最终扣减这类强一致场景。
我现在遇到热点 Key,会先问两个问题。
第一,用户能不能接受短时间旧数据。
第二,缓存重建慢的时候,系统有没有一个人去重建,而不是一群人一起冲上去。
这两个问题想清楚,方案就不会只停在“加个 Redis”这么粗。
很多缓存事故,最后都能归到这三种现场问题里。
不存在的数据反复查,缓存和数据库都没有结果。
热点 Key 刚好过期,一群请求同时去重建。
大量 Key 一起失效,数据库突然被流量接管。
四、缓存雪崩:不是一个 Key,是一片缓存一起倒下
缓存雪崩更像一次集体失效。
活动开始前,系统批量预热了几万条商品缓存,统一设置 30 分钟过期。30 分钟后,大量 Key 一起失效,数据库突然迎来一波本来不该它承受的压力。
这个问题的预防很朴素:TTL 要加随机偏移。
基础 TTL:30 分钟
随机偏移:0 到 10 分钟
最终 TTL:30 到 40 分钟除此之外,还要考虑 Redis 高可用、核心数据预热、多级缓存、接口限流和降级。
缓存层出问题时,系统不能毫无节制地把所有请求放给数据库。否则 Redis 只是第一个倒下的,数据库才是第二个。
这里有一个很现实的取舍:降级一定会影响体验。
比如排行榜先展示旧数据,活动配置先走本地快照,部分非核心模块临时隐藏。听起来都不完美,但比数据库被打挂以后整个链路一起慢,要好得多。
高并发系统里,有时候“不那么新鲜但还能用”的数据,比“努力追求实时最后全站变慢”更靠谱。
五、命中了缓存,也不代表安全
热点 Key 和大 Key 是另一类容易被忽略的问题。
一个爆款商品详情,即使每次都命中 Redis,也可能把单个 Redis 分片、网络带宽或应用连接打满。解决思路是先识别热点,再隔离热点:本地缓存、多副本缓存、Key 拆分、热点资源单独保护。
大 Key 则是每次操作太重。
比如一个 List 里塞几十万个商品 ID,或者一个 Hash 里放特别多字段。它会带来网络传输、序列化、删除阻塞、持久化和迁移压力。
缓存系统不只怕不命中,也怕命中的东西太热、太大、太集中。
所以缓存命中率高,不一定代表系统就安全。
我以前也容易只看命中率,看到 95% 以上就放心。后来发现热点集中时,命中率很好看,但 Redis 单分片、应用连接池和网络出口照样可能被打满。
指标要连起来看,不能只看一个漂亮数字。
六、缓存不能替数据库做最终判断
缓存和数据库很难做到绝对强一致。大多数业务采用最终一致。
常见策略是:
先更新数据库
-> 再删除缓存
-> 删除失败重试
-> TTL 兜底为什么不是直接更新缓存?
因为缓存可能是复杂聚合结果,直接更新容易遇到并发乱序。删除缓存后,下次读取时重新加载,逻辑更简单,也更容易通过重试补偿。
但库存、余额、支付这类业务不能只靠缓存判断。缓存可以保护读链路,但最终正确性要回到数据库、库存系统或交易系统。
七、我现在会先问几个问题
现在再看缓存,我一般不会先问“要不要加 Redis”。
我会先问:
- 这批请求是不是每次都需要查数据库?
- 不存在的数据会不会被反复查询?
- 热点 Key 过期时,谁负责重建缓存?
- 大量 Key 会不会在同一时间失效?
- 命中率很高的时候,Redis 有没有局部热点?
- 这个业务能不能接受短时间旧数据?
- 涉及库存、余额、支付时,最终判断是不是仍然回到可信数据源?
缓存不是为了快,而是为了让数据库活下来。
本系列:
暂无评论