缓存和数据库一致性怎么保证?
更新时间:2026-09-18。问题驱动短答页,完整并发时序与方案对比见深文 缓存与数据库一致性。
一句话回答
绝大多数读多写少业务用 Cache Aside(旁路缓存)+ 最终一致:读时先查缓存,未命中查数据库并回填;写时先更新数据库,再删除缓存(注意是删,不是更新缓存),再用「删除失败重试 / 订阅 binlog 异步失效 + TTL 兜底」补上极小概率的旧数据窗口。真正强一致(资金、库存)不要依赖缓存,应以数据库事务/行锁/版本号为权威,缓存只作只读副本。
为什么是「删缓存」而不是「更新缓存」
- 缓存常是聚合/计算结果,多个写同时更新容易互相覆盖、固化错误值;
- 写多读少时,更新了一堆根本没人读的缓存,全是无用功;
- 删除是惰性重建,逻辑唯一,下次读时按最新数据生成。
两种写顺序
| 顺序 | 风险 |
|---|---|
| 先更库,再删缓存(推荐) | 仅当「删缓存失败」时旧值残留到 TTL;正常并发下脏读窗口极小 |
| 先删缓存,再更库 | 写库期间并发读 miss,会把旧值回填缓存,脏数据持续到下次失效 |
先删缓存的方案常配延迟双删(删缓存→更库→sleep→再删一次),但 sleep 难精确、二次删也可能失败,只能降低概率,不是强一致。
删缓存失败怎么补
- 删失败投递消息队列,消费者幂等指数退避重试(DEL 天然幂等);
- 或用 Canal/Debezium 订阅 MySQL binlog,监听到变更就异步删对应 key,与业务解耦(有秒级延迟,属最终一致);
- 写库与发消息可用本地消息表/事务消息保证不丢。
读侧与兜底细节
- 热点 key 未命中时加互斥锁,只放一个请求回源,防缓存击穿;
- 回填 value 带 version,用 Lua 做「仅当新版本 ≥ 缓存版本才写入」,防乱序回填旧盖新;
- TTL 加随机抖动并始终设置——它是删除链路全部失效时的最终防线。
什么时候必须更强
余额、库存扣减等不允许旧读的路径:以数据库为唯一权威(事务、行锁、UPDATE ... WHERE version=? 的 CAS),缓存只用于非关键展示或写后失效;必要时写后短窗口强制读主,规避主从延迟把从库旧值回填。
相关与延伸
完整时序图、延迟双删边界、binlog 失效架构和常见误区见 缓存与数据库一致性;缓存异常模式见 缓存穿透/击穿/雪崩;binlog 链路(Canal/Debezium)见 CDC 数据同步;热点治理见 BigKey/HotKey 诊断。
总结
用 Cache Aside:读 miss 查库回填、写时先更新数据库再删除缓存(删而非更新,避免并发覆盖与无用计算);删除失败用幂等重试或订阅 binlog 异步失效补偿,回填带版本号防止旧值盖新值,TTL 加随机抖动并作为最终兜底;这套方案保证的是秒级最终一致,资金库存等强一致路径必须以数据库事务/锁/CAS 为权威,缓存只做只读副本。