一、核心问题与设计原则
首先要明确:绝对的一致性在分布式系统中很难做到,通常追求的是"最终一致性" (即经过一段时间后,缓存和数据库数据会统一)。
设计原则:
- 先保证数据正确性,再追求性能;
- 避免"缓存击穿/雪崩/脏数据";
- 优先选择实现简单、运维成本低的方案。
二、主流一致性方案(按常用程度排序)
方案1:Cache Aside(旁路缓存)------ 最常用、最基础
这是业界最主流的方案,核心是"操作数据库后,再操作缓存",分读、写两个流程:
1. 读流程(Cache Miss 时)
命中
未命中
应用读取缓存
直接返回数据
读取数据库
将数据写入缓存(设置过期时间)
返回数据
2. 写流程(更新/删除数据时)
更新数据库
删除缓存(而非更新缓存)
🔑 关键:写操作时删除缓存,而非直接更新缓存
原因:
- 避免"并发写"导致的缓存覆盖错误(比如两个线程同时更新数据库和缓存,顺序错乱);
- 减少无效更新(比如缓存还没被读取就更新,浪费资源)。
适用场景:
- 读多写少的场景(比如商品详情、用户信息);
- 对一致性要求不是极致高(允许短时间缓存缺失)。
注意事项:
- 必须给缓存设置合理的过期时间(兜底方案,即使删除缓存失败,过期后也会自动刷新);
- 避免"缓存删除失败":可增加重试机制(比如消息队列),或定时任务校验。
方案2:更新缓存 + 数据库(不推荐,仅作对比)
这是新手容易想到的方案,但问题很多,核心流程:
写流程:更新数据库 → 更新缓存
缺点:
- 并发更新问题:线程1更新数据库→线程2更新数据库→线程2更新缓存→线程1更新缓存,导致缓存数据回退;
- 无效更新:缓存还没被读取就被更新,浪费资源;
- 缓存数据冗余:如果数据很少被读,更新缓存无意义。
结论:仅适用于"写极少、读极多"且并发低的场景,几乎不推荐。
方案3:Canal 监听binlog异步更新缓存(最终一致性)
这是基于数据库binlog的异步方案,适合"写多读少"或"对一致性要求稍高"的场景:
核心流程:
应用更新数据库
数据库写入binlog
Canal监听binlog
解析binlog获取数据变更
异步更新/删除缓存
优势:
- 解耦:应用只关注数据库,缓存更新由独立服务处理;
- 避免并发问题:binlog是顺序的,异步更新不会出现顺序错乱;
- 可重试:binlog消费失败可重新消费,保证最终一致性。
适用场景:
- 写多读少的场景(比如订单、交易数据);
- 跨系统数据同步(比如缓存和数据库不在一个服务)。
注意事项:
- 存在短暂延迟(毫秒级),不是实时一致;
- 需要部署Canal等中间件,增加运维成本;
- 要处理binlog解析错误、重复消费等问题。
方案4:分布式事务(2PC/TCC)------ 强一致性(极少用)
这是追求"强一致性"的方案,核心是"数据库和缓存的操作在一个事务中,要么都成功,要么都失败":
核心思路:
- 2PC(两阶段提交):引入协调者,先让数据库和缓存"准备提交",确认都准备好后,再"正式提交";
- TCC(补偿事务):Try(尝试操作)→ Confirm(确认操作)→ Cancel(取消操作),如果缓存更新失败,回滚数据库操作。
缺点:
- 性能极低:分布式事务会阻塞线程,降低系统并发;
- 实现复杂:需要引入Seata等框架,开发和运维成本高;
- 容易出现"事务阻塞""数据锁等待"。
适用场景:
- 金融、支付等对数据一致性要求极高的场景(且并发量低);
- 绝大多数业务场景不推荐使用。
方案5:延迟双删(Cache Aside 优化版)------ 解决并发写问题
针对Cache Aside在高并发下可能出现的"缓存脏数据"问题(比如读请求在写请求删除缓存前读取了旧数据),优化方案是"延迟删除缓存":
核心流程:
1. 更新数据库;
2. 删除缓存;
3. 延迟N毫秒(比如500ms)后,再次删除缓存;
🔑 关键:延迟的时间要大于"读请求从数据库加载数据到缓存的时间",确保覆盖并发场景下的脏数据。
适用场景:
- 高并发读写的场景(比如秒杀商品库存);
- 基于Cache Aside方案,解决极端并发问题。
注意事项:
- 延迟时间需要压测确定(比如500ms~1s),不能太长(影响性能)也不能太短(无效);
- 延迟删除可通过线程池、消息队列实现(避免阻塞主线程)。
三、常见问题与兜底方案
-
缓存删除失败怎么办?
- 重试机制:用Redis的PUB/SUB、RocketMQ等消息队列,失败后重试;
- 定时校验:后台定时任务对比缓存和数据库数据,不一致则修复;
- 过期时间:所有缓存必须设置过期时间,即使删除失败,过期后也会自动刷新。
-
并发读写导致的脏数据?
- 优先用延迟双删;
- 对热点数据加分布式锁(比如Redis锁),保证读写串行化(注意锁粒度,避免性能问题)。
-
缓存击穿(缓存失效后大量请求打数据库)?
- 加互斥锁:同一时间只有一个线程去数据库加载数据,其他线程等待;
- 热点数据永不过期:结合定时任务主动更新。
四、方案选择总结
| 方案 | 一致性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| Cache Aside | 最终一致 | 高 | 低 | 读多写少、大部分业务场景 |
| Canal 异步更新 | 最终一致 | 中 | 中 | 写多读少、跨系统同步 |
| 分布式事务 | 强一致 | 低 | 高 | 金融/支付、极低并发 |
| 延迟双删 | 最终一致 | 中 | 低 | 高并发读写、优化Cache Aside |
总结
- 首选方案 :绝大多数场景用 Cache Aside(旁路缓存),核心是"写删缓存、读加载缓存+过期时间",简单且能满足90%以上的业务需求;
- 优化方案 :高并发读写场景叠加 延迟双删 ,写多读少场景用 Canal监听binlog;
- 兜底保障:所有缓存必须设置过期时间,增加删除重试和定时校验机制,避免脏数据长期存在。