主题切换
双写一致
一、基础定义
双写一致性:MySQL数据发生增删改时,同步维护Redis缓存,保证DB与缓存数据相同,避免缓存遗留旧脏数据。
读操作通用逻辑
请求优先查询Redis:
- 缓存命中 → 直接返回数据;
- 缓存未命中 → 查询MySQL,结果回写入Redis并配置过期时间后返回。
二、方案1:延时双删(同步方案,中小项目常用)
执行顺序
第一次删缓存 → 更新MySQL → 延时等待 → 第二次删缓存
- 首次删缓存:清空旧缓存,防止后续读请求命中过期数据;
- 更新数据库:落地最新数据至MySQL;
- 延时二次删缓存:等待并发读请求查库回填缓存、主从数据同步完成,删除并发写入的脏缓存。
缺点:仍存在极小概率脏数据,仅能实现最终一致,无法保证强一致。
三、方案2:读写锁方案(读多写少业务,强一致性首选)
依托Redisson读写分布式锁,遵循规则:读读共享、读写互斥、写写互斥
写流程(加排他WriteLock)
加排他锁 → 修改数据库 → 删除缓存 → 释放锁 加锁期间所有读请求阻塞,避免改库过程中旧数据回填缓存。
读流程(加共享ReadLock)
加共享锁 → 查询缓存,缓存失效则查库并更新缓存 → 释放锁 多个读线程可同时加读锁并发读取,互不阻塞。
优点:实现强数据一致性; 缺点:写操作会阻塞读请求,高并发写场景性能较差,适合商品详情、资讯等读多写少业务。
四、方案3:MQ异步更新缓存(分布式中大型项目,异步最终一致)
流程
- 业务服务修改数据,先落库MySQL,再向MQ发送缓存更新消息;
- 独立缓存服务消费MQ消息,执行删除或更新Redis缓存操作。
关键点:需保障MQ消息可靠性,搭配生产者确认、消息持久化、死信队列,避免消息丢失造成缓存更新失败。
优点:业务线程无阻塞,服务解耦,任务失败支持重试; 缺点:额外依赖MQ中间件,整体架构复杂度提升。
五、方案4:Canal监听Binlog(零侵入业务,大厂主流)
流程
- 业务服务正常修改并写入MySQL,业务代码无需操作缓存、发送消息;
- Canal伪装成MySQL从节点,实时监听Binlog二进制日志,捕获数据变更事件;
- Canal推送变更数据,由缓存服务接收并更新/删除Redis缓存。
优点:完全解耦业务代码,数据变更与缓存更新互不影响; 缺点:引入Canal组件,增加运维成本。
六、四种方案选型总结
| 方案 | 适用场景 | 一致性 | 性能 |
|---|---|---|---|
| 延时双删 | 单体小型项目、低并发 | 最终一致 | 优秀 |
| 读写锁 | 读多写少、要求强一致(商品/资讯) | 强一致 | 写阻塞读,写多场景性能差 |
| MQ异步 | 中大型分布式项目、高并发 | 最终一致 | 优秀,异步解耦 |
| Canal Binlog | 大型分布式、零代码侵入需求 | 最终一致 | 最优,业务无感知 |
七、面试高频问题解析
问:什么是缓存与数据库双写一致性?
答: 对MySQL执行增删改操作时,同步维护Redis缓存,让数据库和缓存的数据保持一致,防止缓存中存在过期脏数据。常规读取逻辑为先查缓存,未命中再查询数据库并回填缓存。
问:简述延时双删的执行流程和优缺点?
答: 流程为先删除缓存,再更新数据库,等待一段时间后再次删除缓存。该方案实现简单,适合中小项目;但无法做到强一致性,依旧存在产生脏数据的可能,仅能保证最终一致。
问:读写锁方案如何保证缓存强一致性,适用什么场景?
答: 使用分布式读写锁,读写互斥、写写互斥,写数据时加排他锁阻塞所有读请求,杜绝旧数据回填缓存。该方案可以实现强一致,缺点是写操作会阻塞读请求,更适合商品、资讯这类读多写少的业务。
问:MQ异步更新缓存和Canal监听Binlog方案有什么区别?
答: MQ方案需要业务代码主动发送消息,依赖消息队列实现异步更新;Canal通过监听MySQL二进制日志感知数据变更,业务代码完全无需改造,侵入性更低。二者都属于异步最终一致方案,Canal更适合追求代码解耦的大型项目。
问:四种双写一致性方案该如何选型?
答: 小型单体低并发项目选用延时双删;读多写少且要求强一致选用读写锁;中大型高并发分布式项目选用MQ异步方案;大型项目、要求业务代码零侵入则使用Canal监听Binlog。