在后端系统架构中为了提升读请求性能、减轻数据库压力我们通常会引入 Redis 等分布式缓存将热点数据缓存起来形成「数据库MySQL 缓存Redis」的双层存储架构。而双写一致性就是指在这种架构下当数据发生变更时如何保证数据库中的数据与缓存中的数据保持一致避免出现“缓存存旧值、数据库存新值”或“缓存有值、数据库无值”的脏数据问题确保业务查询结果的准确性。前言MySQL与Redis双写一致性解决方案保证MySQL和Redis双写一致性核心是根据业务对“一致性/性能”的要求选择方案我从核心方案、进阶方案、兜底方案三个维度结合优缺点和底层逻辑给您讲清楚可以根据业务场景选择下述缓存一致性方案缓存双删如果公司现有消息队列中间件可以考虑使用该方案反之则不需要考虑。先写数据库再删缓存这种方案从实时性以及技术实现复杂度来说都比较不错推荐大家使用这种方案。Binlog 异步更新缓存如果希望实现最终一致性以及数据多中心模式该方案无疑是最合适的。一、核心方案延迟双删最常用适配80%场景延迟双删其实是对「先删缓存再更新数据库」方案的优化和兜底而「先更新数据库再删缓存」是另一条独立的主流路径1. 核心流程写操作先删缓存 → 更新数据库 → 延迟N秒如1-5秒再删一次缓存读操作缓存命中直接返回 → 缓存未命中 → 查数据库 → 回写缓存 → 返回数据2. 各步骤优缺点步骤优点缺点先删缓存避免更新DB后旧缓存被读请求回填极端并发下仍可能出现脏数据更新数据库保证主数据DB准确性单步操作无额外缺点延迟删第二次缓存解决并发场景下的脏数据问题增加少量延迟需控制延迟时长读未命中查DB回写缓存保证缓存最终有数据提升读性能首次读/缓存失效时有DB查询开销不管是「先操作 Redis缓存再操作数据库」还是「先操作数据库再操作 Redis」本质上因为两个操作无法原子化执行且高并发下读 / 写请求的执行顺序不可控所以必然存在脏数据的可能。我用两个最典型的并发时序场景就能说清原因场景 1先删除缓存再操作数据库左侧时序线程 1写请求执行第 1 步 → 删除缓存缓存变为空线程 2读请求执行第 2 步 → 查询缓存未命中去数据库查到旧值20线程 2读请求执行第 3 步 → 把旧值20写入缓存线程 1写请求执行第 4 步 → 更新数据库为v20这里图里初始 DB 是 20实际业务里是从旧值更新为新值核心逻辑不变✅结果数据库是新值缓存里被写入了旧值 → 出现脏数据。场景 2先操作数据库再删除缓存右侧时序线程 1读请求执行第 1 步 → 查询缓存未命中去数据库查到旧值20线程 2写请求执行第 2 步 → 更新数据库为v20实际业务是从旧值更新为新值线程 2写请求执行第 3 步 → 删除缓存缓存变为空线程 1读请求执行第 4 步 → 把刚才查到的旧值20写入缓存✅结果数据库是新值缓存里被写入了旧值 → 同样出现脏数据。不管先操作哪一方问题的根源都是操作非原子性缓存和 DB 的操作是两步独立操作中间有时间窗口并发时序不可控高并发下读请求可能卡在 “查 DB 拿到旧值” 和 “回写缓存” 之间写请求刚好完成了更新导致旧值被写回缓存缓存回写机制读请求缓存未命中时会自动查 DB 并回写缓存这个 “回写” 动作是脏数据产生的最后一环。这也是为什么单纯的 “先操作 A 再操作 B” 解决不了问题需要延迟双删、加锁、异步通知等方案兜底的核心原因。3. 删两次/延迟删除为什么要删两次缓存解决“读请求在写请求更新DB前已查到旧值并准备回写缓存”的极端场景例① 读请求缓存失效 → ② 读DB拿到旧值 → ③ 写请求删缓存 → ④ 写请求更DB → ⑤ 读请求把旧值写入缓存 → ⑥ 延迟删缓存清空这个旧值。第二次删除是为了兜底清空并发场景下被误写入的旧缓存。为什么要延迟删除延迟时长需覆盖“读请求查DB回写缓存”的耗时一般1-5秒确保能删掉“被并发读请求回填的旧缓存”如果立刻删第二次可能读请求还没完成回写删了也没用。二、进阶方案互斥锁/读写锁强一致性场景互斥锁强一致、性能低读多写少一般采用缓存1. 互斥锁分布式锁如Redisson RLock流程写操作加排他锁→ 删缓存 → 更新DB → 释放锁读操作加共享锁→ 缓存未命中时加排他锁查DB回写缓存。优点严格保证数据强一致无脏数据缺点加锁会阻塞请求降低并发性能可能出现死锁需设置锁超时。2. 读写锁Redisson ReadWriteLock共享锁读锁多个读请求可同时加锁不阻塞保证读性能排他锁写锁写请求加锁后阻塞所有读/写请求保证写操作原子性适用场景读多写少、对一致性要求极高的场景如库存、优惠券缺点写请求会阻塞读请求高并发写场景性能下降。3.代码实现核心说明锁粒度读写锁使用同一个 keyITEM_READ_WRITE_LOCK保证读写互斥、读读共享。写锁逻辑写操作加排他锁期间所有读 / 写请求都会阻塞保证数据更新的原子性更新后删除缓存触发下一次读请求重建缓存。读锁逻辑读操作加共享锁多个读请求可并发执行写锁释放前读锁会等待避免读到脏数据。兜底保障finally块中释放锁确保即使业务异常也不会导致锁泄漏。排他锁写锁)publicvoidupdateById(Integerid){// 获取读写锁RReadWriteLockreadWriteLockredissonClient.getReadWriteLock(ITEM_READ_WRITE_LOCK);// 获取写锁RLockwriteLockreadWriteLock.writeLock();try{// 加写锁writeLock.lock();System.out.println(writeLock...);// 1. 更新业务数据示例模拟更新商品信息ItemitemnewItem(id,华为手机,华为手机,5299.00);try{// 模拟业务处理耗时Thread.sleep(10000);}catch(InterruptedExceptione){e.printStackTrace();}// 2. 删除缓存redisTemplate.delete(item:id);}finally{// 释放写锁writeLock.unlock();}}共享锁读锁publicItemgetById(Integerid){// 获取读写锁RReadWriteLockreadWriteLockredissonClient.getReadWriteLock(ITEM_READ_WRITE_LOCK);// 获取读锁RLockreadLockreadWriteLock.readLock();try{// 加读锁readLock.lock();System.out.println(readLock...);// 1. 先查询缓存Itemitem(Item)redisTemplate.opsForValue().get(item:id);if(item!null){// 缓存命中直接返回returnitem;}// 2. 缓存未命中查询业务数据示例模拟从数据库查询itemnewItem(id,华为手机,华为手机,5999.00);// 3. 写入缓存redisTemplate.opsForValue().set(item:id,item);// 4. 返回数据returnitem;}finally{// 释放读锁readLock.unlock();}}三、高级方案异步通知解耦高可用1. MQ异步通知方案流程更新DB → 发送“删除缓存”消息到MQ → 消费端监听消息 → 删除缓存核心优势利用MQ的持久化重试机制保证缓存最终被删除且业务线程无需等待缓存操作接口响应快缺点有代码侵入需业务主动发消息消息延迟可能导致短暂不一致。2. Canal阿里异步同步方案原理Canal伪装成MySQL从库读取MySQL的Binlog数据变更日志 → 解析Binlog → 异步更新/删除Redis缓存优点零业务代码侵入Binlog天然有序能保证更新顺序一致性更高缺点部署维护成本高Binlog解析有轻微延迟毫秒级不适合极致强一致场景。四、补充方案定时任务兜底对核心数据定时从DB全量同步到Redis修正可能的不一致适合数据变更频率低的场景TTL兜底给缓存设置合理过期时间即使出现脏数据也会自动过期保证最终一致全量更新不推荐写操作直接更新DB更新缓存优点是简单缺点是性能差很多更新无意义、并发下易出现数据覆盖。五、总结性能优先、允许短暂不一致选延迟双删核心 MQ/Canal兜底强一致性优先、读多写少选Redisson读写锁不想侵入业务代码、追求极致解耦选Canal方案中小规模业务延迟双删即可满足需求无需过度设计。关键点回顾延迟双删是基础方案两次删除的核心是解决“并发读回填旧缓存”问题延迟时长需覆盖读请求回写缓存的耗时互斥锁/读写锁保证强一致但牺牲性能适配库存、优惠券等核心场景MQ/Canal是异步解耦方案利用MQ可靠性/Canal零侵入特性保证最终一致性。