付同学的技术客栈/

高并发系统里,缓存到底在保护谁?

所属专题 高并发海量数据系统架构 第 2 篇 / 共 11 篇 查看专题目录

活动刚开始的前十分钟,商品详情页先慢了。

下单接口还没到峰值,支付也没出问题,但数据库 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 有没有局部热点?
  • 这个业务能不能接受短时间旧数据?
  • 涉及库存、余额、支付时,最终判断是不是仍然回到可信数据源?

缓存不是为了快,而是为了让数据库活下来。


本系列:

高并发海量数据系统架构 第 2 篇 / 共 11 篇
查看专题目录

留下一条评论

暂无评论