一. 先更新数据库再更新缓存
- 业务代码中先 DB 后 Cache(不推荐)
updateDB();
updateCache();
原因:
- DB成功,Cache失败 缓存不一致 无法进行回滚
为啥使用@Transactional不能保证数据一致性?
@Transactional
public void update() {
updateDB(); // 数据库
updateCache(); // Redis / 本地缓存
}
上面代码的执行顺序:
1.updateDB() → 执行 SQL(未提交)
2.updateCache() → Redis 已更新
3.方法结束
4.事务提交(或回滚)
@Transactional满足条件?
@Transactional 的本质基于数据库的事务。
它只能保证:
1.多条数据库 SQL
2.在同一个数据源
3.同一个线程
4.同一个事务上下文
- 业务代码中先 DB ,再删除 Cache(推荐)但是会出现短暂不一致
@Transactional
public void update() {
updateDB();
}
public void afterUpdate() {
deleteCache();
}
流程:
更新 DB(事务)
提交成功
删除缓存
下次读 → 回源 DB → 重建缓存
二. 先更新缓存再更新数据库
结论:
先更新缓存再更新数据库的做法,永远无法保证数据严格的一致性。
原因:事务A更新缓存成功,更新数据库失败,就会导致redis和mysql的
数据不一致。
绝大多数场景不使用先更新缓存再更新数据库。
特殊场景:缓存为主,DB为辅。
思路:缓存是主存储,数据库只是持久化备份
适用场景:
游戏排行榜 / 计分系统
高频访问、对性能要求极高的热点数据
热点缓存需要立即生效,DB 延迟可接受
高吞吐、读多写少的场景
DB 写操作是瓶颈,先更新缓存可以减少 DB 压力
三. 双删策略
并发问题:并发读写导致的缓存脏数据
假设有 三个事务 A、B、C,涉及 DB + Redis 缓存?
事务A:
deleteCache() // 删除缓存
updateDB() // 更新数据库
DB还没有更新完成
还没执行第二次删除缓存
事务 B(读操作):
cache miss // 缓存已被删除
read DB // 读到旧值(事务 A 还没提交/更新完成)
write cache // 回写旧值到缓存
事务C:
读取缓存中旧的数据
四 . canal监听Binlog日志
双删策略对比canal监听Binlog日志的劣势?
双删策略缺点?
- 时间窗口不太好控制
如果 sleep 太短 → 不一致仍可能
如果 sleep 太长 → 延迟大,业务性能受影响
- 业务耦合
每个更新操作都要手动加双删逻辑
多服务、多表、多缓存的情况下,维护成本高
- 无法扩展到多个下游系统
比如缓存 + Elasticsearch + MQ
双删只能针对单个缓存,无法保证其他下游系统同步
canal 监听 Binlog 的优势?
DB update → MySQL binlog → Canal → 消费端处理 → 更新缓存 / 下游系统
- 完全解耦业务
业务代码只管 DB,不用关心缓存
不会增加每个更新的复杂度
- 可扩展性
一个Canal消费端可以同时同步缓存,搜索引擎,消息队列等。
多个下游系统共享同一条 Binlog 数据,保证最终一致性
统一容错处理
Canal 自带消费重试
消费失败可以回溯 binlog
远比双删"sleep+delete"可控
五、为什么大规模系统不用双删而用 Canal
维度 双删 Canal
业务 耦合 强,每个更新都要写双删 弱,业务只管 DB
多下游系统 每个系统都要自己写双删 一条 Binlog 可以同步多个系统
可靠性 不可控,sleep 仍可能不一致 可控,Binlog 消费可重试
扩展性 差 好,适合微服务和大流量系统
可维护性 差 好, 统 一逻辑
双删适合小型、单服务、低流量场景;
Canal + Binlog 更新缓存适合大型、分布式、高并发场景,因为它解耦、统一、可重试,维护成本更低。