坦白说,这个问题本质上不是一个纯技术方案题,而是高并发场景下,平衡【性能、可用性、一致性强度】的工程题。
核心是先看业务的一致性容忍度,再选对应方案,而且必须做好异常兜底,不然线上一定会出永久脏数据。
在电商场景中,绝大多数情况下优先保证最终一致性,只有金融级核心链路才会考虑强一致性。
先说标准流程:
- 读请求:先查缓存,命中直接返回;没命中就读数据库,把结果写回缓存再返回。
为什么是【删除缓存】,而不是【更新缓存】?
一是并发写场景下,更新缓存一定会有数据覆盖的风险;
二是电商场景很多缓存值是多表关联算出来的,更新缓存的成本极高,不如删除后懒加载;
三是写多读少的场景,频繁更新缓存全是无效操作,浪费性能。
为什么是【先更库,再删缓存】,不能反过来?
如果你刚把缓存删了,数据库还没更新完,这时候高并发的读请求进来,缓存查不到,直接去数据库拉了旧数据,还写回了缓存。
等你数据库更新完,缓存里的旧数据就一直躺在那,除非缓存过期,不然用户一直看到的是错的数据。
但是,如果是先更库再删缓存,只有极小概率会出问题,比如:
读请求缓存没命中,正在读数据库的瞬间,写请求更新了数据库并删了缓存,之后读请求把旧数据写回缓存。
但是,但是啊,这个场景发生的条件太苛刻了,因为写数据库的耗时远大于读数据库,基本卡不中这个时间窗口,线上出问题的概率几乎可以忽略。
就算真的撞上了,还有缓存 TTL 兜底,最多就是短时间不一致,绝对不会出永久脏数据。
其实,这个方案最大的风险,是第二步删缓存失败。
同步删缓存如果失败,立刻把这个key丢到消息队列里做异步指数退避重试,设置最大重试次数,绝对不让缓存操作阻塞主流程。
高并发下的两个进阶方案
第一个是延迟双删,解决基础方案的极端并发问题。“针对商品详情、营销活动这种超高并发读的场景,用延迟双删做加固,流程是:
先删除缓存➡️更新数据库➡️延迟500ms-1s,二次删除缓存。第二次延迟删除,刚好能覆盖掉【数据库更新过程中,读请求写回缓存的旧数据】,彻底解决极端并发的脏数据问题。其中,延迟时间必须大于【业务读请求的最大耗时 + 数据库主从同步延迟】。
第二个是基于Binlog的异步缓存淘汰。用Binlog异步淘汰缓存,完全解耦业务和缓存操作。
业务代码只做一件事:更新数据库,完全不碰缓存。
然后用Canal监听数据库的binlog增量日志,只有主库事务提交成功,才会拿到数据变更事件,再异步去删除对应的缓存key。如果删除失败,就丢进死信队列重试,超过阈值直接触发告警。
强一致性方案
只有像支付账户余额、核心库存扣减这种,一分钱都不能错、零容错的场景,才会考虑强一致性方案,因为它的性能代价极大。用Redisson的分布式读写锁,写请求加写锁,读请求加读锁,读写互斥,强制串行化,彻底避免并发问题。
但这个方案会严重降低系统吞吐量,高并发下很容易出现锁等待、超时问题,所以非核心场景,绝对不会用。
兜底机制
不管方案设计得多完美,线上一定会有意外,比如:
缓存集群炸了、消息队列挂了、binlog消费延迟了,没有兜底,就等着出线上事故。可以采用以下兜底策略:
- TTL兜底:所有缓存key必须设置合理的过期时间,哪怕所有机制都失效了,最多就是一段时间的不一致,缓存一过期,就自动拉取最新数据,绝对不会出现永久脏数据。
- 失败重试机制:所有缓存删除操作,必须有异步重试兜底,不能一次失败就放任脏数据存在。
- 降级开关:大促场景下,如果缓存集群扛不住了,直接开降级开关,所有请求切到读数据库,先保证用户能下单,别的都好说。