Loading...

【面试真题】缓存和数据库的一致性问题如何保证?

坦白说,这个问题本质上不是个纯技术方案题,而是高并发场景下,平衡【性能、可用性、一致性强度】的工程题。

核心是先看业务的一致性容忍度,再选对应方案,而且必须做好异常兜底,不然线上一定会出永久脏数据。

在电商场景中,绝大多数情况下优先保证最终一致性,只有金融级核心链路才会考虑强一致性

先说标准流程

  • 读请求:先查缓存,命中直接返回;没命中就读数据库,把结果写回缓存再返回。
  • 写请求:先更新数据库,再删除缓存。

为什么是【删除缓存】,而不是【更新缓存】?

一是并发写场景下,更新缓存一定会有数据覆盖的风险;

二是电商场景很多缓存值是多表关联算出来的,更新缓存的成本极高,不如删除后懒加载;

三是写多读少的场景,频繁更新缓存全是无效操作,浪费性能。

为什么是【先更库,再删缓存】,不能反过来?

如果你刚把缓存删了,数据库还没更新完,这时候高并发的读请求进来,缓存查不到,直接去数据库拉了旧数据,还写回了缓存。

等你数据库更新完,缓存里的旧数据就一直躺在那,除非缓存过期,不然用户一直看到的是错的数据。

但是,如果是先更库再删缓存,只有极小概率会出问题,比如:

读请求缓存没命中,正在读数据库的瞬间,写请求更新了数据库并删了缓存,之后读请求把旧数据写回缓存。

但是,但是啊,这个场景发生的条件太苛刻了,因为写数据库的耗时远大于读数据库,基本卡不中这个时间窗口,线上出问题的概率几乎可以忽略。

就算真的撞上了,还有缓存 TTL 兜底,最多就是短时间不一致,绝对不会出永久脏数据。

其实,这个方案最大的风险,是第二步删缓存失败。

同步删缓存如果失败,立刻把这个key丢到消息队列里做异步指数退避重试,设置最大重试次数,绝对不让缓存操作阻塞主流程。

高并发下的两个进阶方案

第一个是延迟双删,解决基础方案的极端并发问题。
“针对商品详情、营销活动这种超高并发读的场景,用延迟双删做加固,流程是:

先删除缓存➡️更新数据库➡️延迟500ms-1s,二次删除缓存。
第二次延迟删除,刚好能覆盖掉【数据库更新过程中,读请求写回缓存的旧数据】,彻底解决极端并发的脏数据问题。
其中,延迟时间必须大于【业务读请求的最大耗时 + 数据库主从同步延迟】。

第二个是基于Binlog的异步缓存淘汰。
用Binlog异步淘汰缓存,完全解耦业务和缓存操作。

业务代码只做一件事:更新数据库,完全不碰缓存。

然后用Canal监听数据库的binlog增量日志,只有主库事务提交成功,才会拿到数据变更事件,再异步去删除对应的缓存key。如果删除失败,就丢进死信队列重试,超过阈值直接触发告警。

强一致性方案

只有像支付账户余额、核心库存扣减这种,一分钱都不能错、零容错的场景,才会考虑强一致性方案,因为它的性能代价极大。
用Redisson的分布式读写锁,写请求加写锁,读请求加读锁,读写互斥,强制串行化,彻底避免并发问题。

但这个方案会严重降低系统吞吐量,高并发下很容易出现锁等待、超时问题,所以非核心场景,绝对不会用。

兜底机制

不管方案设计得多完美,线上一定会有意外,比如:

缓存集群炸了、消息队列挂了、binlog消费延迟了,没有兜底,就等着出线上事故。
可以采用以下兜底策略:

  1. TTL兜底:所有缓存key必须设置合理的过期时间,哪怕所有机制都失效了,最多就是一段时间的不一致,缓存一过期,就自动拉取最新数据,绝对不会出现永久脏数据。
  2. 失败重试机制:所有缓存删除操作,必须有异步重试兜底,不能一次失败就放任脏数据存在。
  3. 降级开关:大促场景下,如果缓存集群扛不住了,直接开降级开关,所有请求切到读数据库,先保证用户能下单,别的都好说。
最后更新于 2026-04-05 17:35:33
Code Road Record