上篇文章提到了延时双删仍然会存在删除缓存失败的情况。需要通过重试机制来保障删除缓存的成功率
前情回顾
上回说的小树做了一个缓存的功能,可是出现了数据不一致的情况,经过老大哥的指教,先是想到了旁路缓存的思路,后面还是会存在数据不一致的问题,经过几番优化,又找到了延时双删的策略来减少并发时出现的问题。可是仍然没有解决删除缓存时可能失败的问题,只是提到了可以考虑使用失败重试机制
失败重试的方案
老大哥跟小树说:
失败重试的方法一般分为两类
- 同步重试
- 异步重试
同步重试的实现就很简单了,直接在程序中实现,失败后再次删除,达到一定次数后停止重试。缺点也很明显,并发高的时候对接口性能很大。
异步的实现方式就很多了:
- 通过线程实现
每次新增一个线程来进行重试操作。高并发下容易创建太多线程,会出现OOM问题;
当然你可以通过线程池来管理线程,这样可以避免OOM问题
但通过线程来进行重试的话,无法保留重试的记录,如果服务器重启,尚未重试成功的数据就会丢失 - 重试内容写入表中,通过定时任务执行
- 将重试的内容写入消息中间件中,通过消费消息来删除缓存,由消息中间件来保证消息的可靠消费
小树听了上面的三种方式说:第一种和第二种方法太简单了。你还是给我讲讲消息队列的方法吧
消息队列补偿
通过异步解耦,确保缓存删除操作最终成功。
实现步骤

- 用户操作数据后,程序先更新数据库,然后删除缓存,成功直接返回,失败则放入消息中间件中
- 消费者检测到有消息开始消息消息,执行删除缓存操作,成功后删除这条消息,失败则进行重试,重试达到一定次数后,可根据业务做出处理如写入表中或者直接抛出错误
优缺点
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 高可靠性 | 系统复杂度高 | 分布式系统 |
| 解耦业务 | 消息可能堆积 | 订单/支付系统 |
选型建议:
- 低并发场景:用Redis Stream实现轻量级队列
- 高并发场景:RocketMQ + 死信队列监控
小树听了上面几种方案,又提问道:如果选上面几种方案的话,我需要改造原来的代码逻辑,有没有不需要改动原来代码就可以完成删除缓存的操作
方案四:Binlog监听(Canal)
通过数据库日志驱动缓存更新,实现业务零侵入。
Canal部署架构
具体方案如下:
- 程序只需要更新数据库就好了,其他的操作完全不关心。
- 中间件会伪装为MySQL 的从库,同步订阅binlog日志来获取变更的数据。
- binlog订阅者获取变更的数据,然后删除缓存。
优势:
- 完全解耦业务代码
- 实时性达到毫秒级
优缺点
| 优点 | 缺点 | 适用场景 |
|---|---|---|
| 业务零侵入 | 部署复杂 | 微服务架构 |
| 毫秒级同步 | 需处理DDL事件 | 数据异构同步 |
适用场景:
- 微服务架构中多个服务共享缓存
- 数据异构场景(如ES索引同步)
小树听了总结道:上面这些方案我都理解了,但这些方案好像都只是尽量避免缓存不一致的情况,并没有真正的实现读缓存的时候一定和数据库保持一致
老大哥点了点头,说:是的,数据库追求数据的准确性和持久性,而缓存则追求极致的响应速度。大部分应用缓存的场景都是可以允许短暂的数据不一致的情况的,也就是最终一致性。如果追求强一致性,不可避免的就要牺牲一部分的响应性能。
常见的方案就是加锁,加锁方式又分为乐观锁和悲伤锁两种,这两个概念之前的文章有介绍过,这里简单总结一下各自的特点,就不详细介绍了
乐观锁:版本号控制
通过数据版本校验,实现精准更新。
额外成本:
- 存储开销增加5%-10%
- 所有读写操作需校验版本号
回报:
- 缓存命中率提升40%
- 彻底解决并发更新导致的脏读
终极大招:分布式锁
通过互斥锁确保同一时刻只有一个写操作。
性能数据:
- 吞吐量下降60%(实测)
- 平均响应时间从20ms → 80ms
使用铁律:
- 仅用于库存扣减等核心场景
- 必须设置锁超时时间!
最后抉择:一表帮你选型
| 场景特征 | 推荐方案 | 一致性强度 | 性能影响 |
|---|---|---|---|
| 读多写少 | Cache-Aside | 最终一致 | 无影响 |
| 高并发写入 | 双删+消息队列 | 最终一致+ | 中等 |
| 金融/交易场景 | 分布式锁+版本号 | 强一致 | 高 |
选择方案时,记得问自己一句你的业务真的需要强一致性吗?
