前言
Cache Aside(旁路缓存)模式下,先更新数据库、再删除缓存是互联网最通用的读写方案。很多同学只知道这套流程,却忽略一个致命风险:删除缓存操作失败怎么办?
网络抖动、Redis宕机、超时异常,都会导致DB数据已经更新,但旧缓存残留,产生永久数据不一致。
针对这个问题,业界衍生多层优化方案:本地重试、业务代码发送MQ异步重试,最终大厂生产主流落地方案为 Canal监听binlog + MQ。本文逐层拆解方案优劣、踩坑点、选型依据,同时梳理面试完整答题思路。
前置共识:只要数据库与缓存是两套独立存储,所有异步方案都属于最终一致性,无法彻底消除短暂不一致窗口;我们所有方案目标,是消灭永久性脏数据。
一、基础方案:更新数据库 → 同步删除缓存
标准流程:
- 业务代码执行
update/insert/delete更新MySQL - DB事务提交成功,同步调用Redis删除对应缓存Key
致命缺陷:缓存删除失败,永久不一致
分布式环境网络永远不可靠:
- Redis服务宕机、连接超时、网络闪断;
- 删除缓存请求执行异常,代码捕获异常仅打印日志,无后续补偿。
结果:数据库已经是新数据,缓存持续保留旧值,直到缓存TTL过期,业务持续读取脏数据。
简单优化思路:本地循环重试3次。
局限性:同步重试拖慢接口响应;多次重试依旧失败时,依然没有兜底手段,无法从根源解决问题。
二、进阶方案:业务代码发送MQ,异步重试删除缓存
优化架构:更新数据库 → 业务生产者发送「删除缓存」消息至MQ → MQ消费者执行Redis删除
核心思路
将缓存删除动作异步化,依靠MQ的ACK机制实现可靠重试:
✅ 规范:缓存删除成功后,再向MQ提交ACK
- 删除缓存失败:不确认消息,消息保留在队列,持续重试;
- 删除缓存成功:提交ACK,消息正常消费完成;
- 超过最大重试阈值,消息转入死信队列,触发告警,人工介入。
⚠️ 重大易错坑:禁止先ACK、后删缓存
一旦先确认消息,删除缓存操作失败,消息被标记消费完成,永远不会重试,直接永久脏数据。
方案现存硬伤
- 业务代码强侵入
项目所有更新数据库的接口,都需要手动编写发送MQ逻辑。开发人员一旦漏写任意一处更新逻辑,缓存永远无法失效。后续迭代新增更新逻辑,极易遗忘。 - 事务时序风险(经典并发漏洞)
错误时序:业务线程先发MQ消息 → 提交数据库事务
MQ消费者立刻收到消息,删除缓存;此时数据库事务尚未提交。其他查询请求命中缓存miss,读取DB旧数据回填缓存,产生永久脏数据。 - DB与MQ消息投递一致性难题
更新数据库事务成功,发送MQ消息网络异常失败。此时数据库数据更新完成,但是删除缓存消息丢失,缓存旧数据无法清理。
想要解决该问题,需要引入事务消息、本地消息表,大幅提升开发复杂度。
小结:业务直发MQ可以解决「单次删缓存失败重试」问题,但无法解决代码漏写、消息早于事务提交、消息投递失败等致命隐患。
三、过渡方案:Canal直连Redis(不推荐生产使用)
架构链路:MySQL binlog → Canal → 直接调用服务删除Redis缓存
很多人会想到:既然binlog可以捕获数据变更,直接让Canal调用删缓存接口行不行?
技术上可以跑通,但生产环境缺陷明显:
- 无可靠重试机制
删除缓存失败时,Canal缺少持久化重试队列,这条binlog事件处理失败后,没有机制再次执行删除缓存,事件丢失。 - 无法削峰
数据库大批量更新产生海量binlog,Canal瞬时发起大量缓存删除请求,直接压垮Redis。 - 下游故障无法容错
缓存服务重启维护期间,请求直接报错,变更事件全部丢失,没有地方临时存储任务。 - 扩展性差
后续如需同步数据到ES、数仓,无法实现一份变更事件多端分发。
👉 适用场景:测试环境、低并发业务,允许少量数据不一致。金融、交易等高可靠场景禁止使用。
四、生产最优方案:Canal + MQ 实现缓存一致性兜底
完整链路:
MySQL数据更新 → 事务提交产生binlog → Canal伪装MySQL从库捕获binlog → 推送变更事件到MQ → MQ消费者监听消息,执行Redis缓存删除
四大核心优势
1. 零业务代码侵入,杜绝人为编码疏漏
业务只需要正常编写数据库CRUD,不需要新增任何发送消息代码。不存在开发人员漏写消息发送逻辑导致的缓存不一致问题。
2. 从根源规避时序漏洞
MySQL机制:事务未提交,不会生成binlog。
只有数据库事务成功落地,Canal才能捕获变更事件。彻底杜绝「消息提前下发、数据库还未更新」引发的旧值回填问题。
3. 依托MQ实现可靠重试、流量削峰、故障容错
- 可靠重试:严格遵循「缓存删除成功再ACK」;失败不确认消息,自动重试;多次失败进入死信告警;
- 流量削峰:数据库瞬时大量更新,消息队列缓冲流量,消费者可控速率消费,保护Redis;
- 故障容错:缓存服务停机维护时,消息持久保存在MQ磁盘;服务恢复后自动消费堆积消息,不会丢失缓存清理任务。
4. 天然支持一对多分发
同一条binlog变更消息,可以被多个消费者订阅:
消费者A:删除Redis缓存
消费者B:同步数据至Elasticsearch
消费者C:数据统计、日志归档
后续新增数据同步需求,无需改动原有业务代码。
重要澄清:Canal+MQ依旧是最终一致性!
很多同学存在误区:使用binlog方案就能实现强一致性。
明确结论:不能!
数据更新完成 → binlog同步至Canal → 投递MQ → 消费者删除缓存,整条链路存在网络延迟。在这段时间窗口内,数据库已经是新数据,缓存依旧是旧值,请求依然会读到脏数据。
两类不一致严格区分:
- 永久性不一致(必须根除):代码漏写、消息时序错乱、消息丢失,缓存永久脏数据;Canal方案彻底消灭该类问题;
- 短暂不一致(无法根除):链路异步延迟带来的时间窗口,所有异步方案(业务发MQ、Canal)都会存在。
通俗总结:业务直发MQ同时存在【永久不一致风险+短暂不一致窗口】;Canal方案仅保留短暂不一致窗口,消除灾难性永久脏数据。
五、各类方案横向对比表
| 方案 | 代码侵入 | 时序风险 | 失败重试能力 | 架构复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 同步删缓存+本地重试 | 低 | 无 | 弱,重试失败无兜底 | 极低 | 小型项目、低并发 |
| 业务代码发送MQ异步删缓存 | 高 | 存在消息早于事务提交风险 | 强 | 中等 | 中小型项目,迭代可控 |
| Canal直连Redis | 无 | 无 | 无重试机制 | 中等 | 测试环境,允许丢消息 |
| Canal + MQ | 无 | 无 | 强 | 较高 | 中大型系统、金融业务、高一致性要求场景 |
六、拓展延伸:金融场景如何选型?
金融系统对数据一致性要求严苛,分两类业务进行取舍:
- 核心资金、账务余额(绝对不能读到脏数据)
直接放弃缓存,读写请求直达MySQL,依靠数据库事务保证强一致。多数银行核心账务系统,资金余额不使用Redis缓存。 - 非核心业务(订单基础信息、活动数据,允许毫秒级短暂不一致)
优先采用 Canal+MQ 方案,保证不会出现永久数据不一致。 - 折中方案:希望使用缓存,杜绝并发回填旧值
对热点数据读写增加分布式锁串行化,消除并发旧值回填漏洞;代价是并发吞吐量下降。
避坑提醒:Seata分布式事务不能用来保证MySQL与Redis一致性!分布式事务仅支持数据库资源,Redis无法纳入XA事务,无法实现原子回滚。
七、面试精简背诵总结(面试直接套用)
Cache Aside模式下先更新数据库再删除缓存,存在缓存删除失败导致永久不一致问题。
- 简单方案:业务代码发送MQ异步重试删缓存,依靠MQ ACK实现失败重试;但存在业务代码侵入、容易漏写发送逻辑、消息早于事务提交引发并发bug,还需要处理数据库与MQ消息投递一致性难题。
- 如果直接使用Canal调用删除缓存接口,缺少重试队列、无法削峰,下游故障容易丢失事件,生产不推荐。
- 生产优选 Canal+MQ 架构:基于binlog捕获变更,零业务代码侵入;事务提交后才生成binlog,规避时序漏洞;依托MQ实现重试、削峰、故障容错,支持多下游消费。
- 注意:该方案依旧属于最终一致性,存在短暂不一致窗口,只能杜绝永久性脏数据,无法做到瞬时强一致。如果业务完全不能容忍任何不一致,应当直接舍弃缓存查询数据库。
八、生产落地最佳实践补充
- MQ消费务必保证幂等:binlog可能重复推送,删除缓存天然幂等,无需额外处理;
- 消费者增加重试退避策略,避免频繁重试冲击Redis;
- 死信消息配置告警,及时人工排查Redis故障、网络问题;
- 合理设置binlog过期时间,避免Canal离线太久丢失日志;
- 缓存删除优先使用异步消费,不要阻塞消息消费主流程。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_68315058/article/details/163398593



