Redis 和数据库双写一致性问题如何解决?

张开发
2026/4/17 10:36:07 15 分钟阅读

分享文章

Redis 和数据库双写一致性问题如何解决?
Redis 和数据库双写一致性问题如何解决在高并发场景下Redis 常被用作数据库的“前置缓存”以提升读取性能。但当数据需要更新时写操作如何保证 Redis 缓存与数据库中的数据最终一致甚至强一致就成了经典的“双写一致性”难题。一、问题根源先操作缓存还是先操作数据库很多同学会本能地想到“先更新数据库再更新缓存”或“先删除缓存再更新数据库”。但这两种方式都存在竞态条件风险操作顺序可能的问题先更新缓存再更新数据库缓存更新成功数据库更新失败 → 数据不一致先更新数据库再更新缓存更新数据库期间有读请求将旧数据载入缓存 → 脏数据先删除缓存再更新数据库删除缓存后、更新数据库前有读请求将旧数据写回缓存 → 脏数据先更新数据库再删除缓存相对安全但删除缓存失败 → 缓存一直是旧数据所以业界主流思路是“先更新数据库再删除缓存”Cache Aside Pattern。而你提供的三种方案正是围绕“如何可靠地删除/更新缓存”展开的。二、方案详解优化与完善方案一先更新数据库再删除缓存 重试 过期兜底这是最简单、最直接的做法核心流程如下Redis缓存数据库应用Redis缓存数据库应用依赖缓存过期时间兜底alt[重试仍失败]alt[删除成功][删除失败]1. 更新数据更新成功2. 删除缓存OK异步重试延迟2秒重试删除失败优化点删除失败后的重试不能无限进行建议设置最大重试次数如3次。重试间隔可以采用指数退避2秒、4秒、8秒避免对 Redis 造成压力。一定要给缓存设置合理的过期时间如30分钟作为最后的“兜底策略”保证即使所有删除都失败数据最终也会因过期而重新从 DB 加载。优点实现简单无需引入额外组件。缺点重试过程可能短暂影响一致性高并发下仍有极小概率读到旧数据在删除缓存之前有读请求。方案二先更新数据库再发 MQ 消息由消费者删除/更新缓存将“删除缓存”这一操作从主线程剥离通过消息队列RabbitMQ、RocketMQ、Kafka 等异步执行。流程如下是否是否应用发起写请求更新数据库更新成功?发送MQ消息包含key或数据返回失败主线程返回成功MQ消费者拉取消息删除/更新Redis缓存操作成功?结束重试队列或记录失败日志优化点消息体建议携带cache_key、operationdelete/update、timestamp、retry_count。消费者需要保证幂等多次删除同一 key 无害。如果业务需要“更新缓存”而非删除需谨慎更新缓存时可能拿到旧值建议还是以删除为主让读请求主动加载最新数据。可以结合 RocketMQ 的事务消息保证数据库更新和消息发送的原子性避免数据库成功但消息未发。优点解耦删除失败不影响主流程可通过 MQ 的重试机制保证最终一致。缺点引入 MQ 增加系统复杂度和延迟。方案三监听 MySQL Binlog异步解析并修改缓存这是一种“无入侵”的最终一致性方案利用 Canal、Debezium 等组件伪装成 MySQL slave解析 binlog 并推送到消息队列或直接更新缓存。业务层写操作产生binlog解析事件删除/更新应用MySQLCanal/Debezium消息队列缓存同步服务Redis时序图以 Canal 为例Redis缓存同步服务MQCanalMySQL应用Redis缓存同步服务MQCanalMySQL应用UPDATE table SET ... WHERE id?OK推送binlog事件解析成行变更发送变更消息消费消息DELETE key 或 SET新值优化点binlog 是顺序的可以保证消息的顺序性避免并发更新导致的乱序问题。建议只监听需要的表减少无用解析。如果更新操作频繁可以批量处理例如每100条 binlog 事件执行一次缓存更新。缓存更新策略删除优于更新因为更新需要知道新值而新值从 binlog 中可以直接拿到Canal 会输出变更后的整行数据此时可直接执行 SET但要考虑缓存与数据库数据格式转换问题。优点对业务代码零侵入天然保证数据库变更一定会被处理适合异构系统同步。缺点延迟相对较高毫秒~秒级组件多运维复杂。三、其他补充方案横向对比除了你提到的三种实际工程中还有以下常见方案3.1 延迟双删先删缓存再更新 DB再删一次# 伪代码redis.delete(key)db.update(data)time.sleep(0.1)# 等待可能存在的并发读请求将旧数据写回缓存redis.delete(key)适用场景对一致性要求中等且无法忍受“先更新 DB 再删缓存”导致的短暂不一致读旧数据。注意sleep 时间需根据业务读耗时估算通常 100~200ms过大会影响写性能。3.2 分布式读写锁强一致性使用 Redisson 等工具对 key 加读写锁读请求加读锁写请求加写锁。写锁会阻塞所有读锁保证写期间无读操作从而避免脏数据。优点强一致性。缺点性能差吞吐量下降严重一般不用于高并发缓存场景。3.3 对比总结表方案一致性强度性能影响实现复杂度是否需要额外组件删除重试过期最终一致弱低低无MQ异步删除最终一致中中MQBinlog同步最终一致顺序保证低业务无感高CanalMQ延迟双删最终一致时间窗口更窄低低无读写锁强一致极高中分布式锁如Redisson四、选型建议并发量低、一致性要求不苛刻方案一删除重试过期足够代码简单。要求高可用不能容忍缓存删除失败方案二MQ 异步删除同时给缓存加过期时间作为兜底。已有异构数据同步需求或希望业务无感方案三binlog 监听适合数据中台、CQRS 场景。对短期不一致零容忍如金融、库存扣减考虑分布式锁或直接读数据库放弃缓存。五、总结Redis 与数据库的双写一致性问题没有“银弹”需要在一致性、性能、复杂度之间做权衡。你提出的三种方案涵盖了从简单到复杂的演进路径同步删除 重试 过期兜底——入门级大部分场景够用。MQ 异步删除——解耦 可靠适合对删除成功率要求高的场景。Binlog 监听——终极解耦适合大规模分布式系统。最后记住一条核心原则缓存只是加速数据库才是真相。无论采用何种方案务必为缓存设置合理的过期时间给数据一致性留一条最后的退路。附常见问题Q先更新数据库再删缓存如果删除失败怎么办A重试 MQ 过期兜底。Q为什么不先删缓存再更新数据库A会导致并发读请求将旧数据写回缓存产生脏数据。QCanal 监听 binlog 会不会有重复消费A会需要消费者做幂等设计如使用唯一事件 ID 或 version 字段。

更多文章