
如何保证 Redis 和 MySQL 的数据一致性标准八股文的答案先删缓存再更新数据库然后延迟一段时间再删一次缓存。听起来逻辑挺对的步骤也不复杂。但在高并发的生产环境里这个方案简直就是坑。要删两次是因为在步骤 1 删完缓存和步骤 2 更新完数据库之间有一个时间窗口。在这个窗口内另一个读请求可能查到了数据库的旧值然后把旧值又写回了缓存。第二次删除就是为了把这个被回填的脏缓存给清掉。逻辑上好像没毛病但问题出在细节里。延迟时间不好定休眠 N 毫秒这个 N 又该定多少呢如果设得太短还没等读请求把旧值写回缓存第二次删除就执行了白忙活如果设得太长在你休眠的这段时间里所有读请求都读到脏缓存脏缓存永不刷新通常建议 N 设成读请求的耗时 几百毫秒但读请求的耗时在高并发下波动很大平时 50ms 高峰期可能 500ms。设个固定值要么平时浪费要么高峰期覆盖不到。更要命的是这个延迟时间一旦定了就是硬编码在代码里的。数据库换了、网络抖了、机器扩容了全得重新评估。线程 sleep 阻塞业务最直接简单的实现就是在业务线程里Thread.sleep(500)。在高并发场景下业务线程是非常宝贵的资源一个写请求进来处理完数据库更新然后线程就在那傻等 500ms。如果每秒有 1000 个写请求就有 1000 个线程同时在 sleep线程池很快就被占满后面的请求全部排队甚至超时。要不我们用异步用 MQ 发个延迟消息来做第二次删除可以是可以但这又引入了 MQ 的复杂度MQ 消费失败了怎么办消费延迟了怎么办为了一个缓存一致性引入了一整套消息中间件的运维成本。第二次删除也可能失败网络又不是 100% 可靠的第二次删除 Redis 的操作可能因为网络抖动、Redis 短暂不可用、超时等原因失败失败了怎么办加重试重试几次重试间隔多久每一层兜底方案都会引入新的复杂度最后你会发现为了保证一个删除操作的可靠性你写了一大坨重试 补偿逻辑。并发写的坑延迟双删只考虑了一个写请求 一个读请求的并发场景两个写请求并发到来时会发生什么假设有两个写请求A 和 B按照时间顺序这个场景下没什么问题。但如果时序稍微变一下但请求 B 是后发起的业务语义上最终值应该是 Y而数据库里存的却是 X。这已经不是缓存一致性的问题了是数据库本身的并发写顺序问题延迟双删根本管不了这个。推荐的方案旁路缓存策略核心就两句话先读缓存缓存没有就读数据库读完写入缓存先更新数据库再删除缓存。注意是先更新库再删缓存不是先删缓存再更新库。为什么这个顺序更好因为数据库更新和缓存删除之间的时间窗口通常极短微秒级在这个窗口内恰好有读请求命中旧缓存的概率很低。而先删缓存再更新库的时间窗口数据库写入耗时要大得多。如果还想要更强的一致性保证可以参考下面的方案Cache Aside先更新库再删缓存适用于大多数业务场景Cache AsideTTL 兜底适用于允许短暂不一致的场景Canal 监听 binlog MQ 异步更新适用于高一致性要求的场景写请求加分布式锁串行化适用于并发写冲突严重的场景延迟双删不是说完全不能用在低并发、对一致性要求不高的场景下它确实简单好理解。但一旦并发上来了它的每一个假设都可能被打破。如果对一致性的要求高到需要延迟双删那说明你应该用更靠谱的方案了如果你对一致性的要求没那么高那单纯的先更新库再删缓存就够了延迟双删刚好卡在一个尴尬的位置上。