缓存与数据库双写不一致问题及终极解决方案(高频面试题)

大家好,今天我们来彻底吃透后端高频核心问题:缓存与数据库双写数据不一致!

这是面试必问、生产必踩坑的经典问题,几乎所有高并发项目(电商、秒杀、资讯、用户中心)都会遇到。本文从零拆解问题根源,手把手分析所有解决方案的优缺点、适用场景,附带清晰流程图、避坑要点,新手也能完全看懂,文末附生产选型总结!

一、前言:为什么会出现双写不一致?

1.1 缓存+数据库架构初衷

在后端开发中,为了提升查询性能、降低数据库压力 ,我们都会采用 MySQL + Redis 架构:

  • 读请求:优先查Redis缓存,命中直接返回,未命中查询MySQL,再回填缓存

  • 写请求:更新数据时,需要同时操作MySQL和Redis

但问题随之而来:数据库和缓存是两个独立的存储组件,无法做到原子性同时更新 ,一旦两次操作执行顺序错乱、并发冲突、操作失败,就会出现缓存存旧数据、数据库存新数据的双写不一致问题。

1.2 核心不一致场景

所有不一致问题,都源于并发读写冲突操作非原子性,典型场景:

  1. 线程A更新数据库,还没来得及操作缓存,线程B读取旧缓存数据

  2. 先删缓存、后更新数据库的间隙,有请求回填旧缓存

  3. 更新数据库成功,但缓存删除/更新失败,永久脏数据

  4. 高并发读写穿插执行,打乱双写顺序

二、四种错误双写方案(避坑重点)

很多新手会写出四种错误的双写逻辑,看似没问题,实则高并发必崩,下面逐一拆解!

2.1 方案一:先更新缓存,再更新数据库

执行流程:更新Redis → 更新MySQL

致命问题:数据丢失+永久不一致

如果缓存更新成功,数据库更新失败,缓存是新数据,数据库是旧数据,后续所有读请求都会读到错误缓存数据,数据库永久无法同步。

2.2 方案二:先更新数据库,再更新缓存

执行流程:更新MySQL → 更新Redis

两大致命问题

  1. 并发覆盖问题 :高并发下,两个更新请求穿插执行 线程A(更新旧值10→20)、线程B(更新10→30) A更新数据库成功,B更新数据库成功 B先更新缓存为30,A后更新缓存为20 最终数据库30,缓存20,永久脏数据

  2. 无效更新开销:频繁更新但无人查询的数据,反复更新缓存纯属浪费性能

2.3 方案三:先删除缓存,再更新数据库

执行流程:删除Redis → 更新MySQL

核心漏洞:读写并发冲突,百分百出现不一致

并发场景复现

  1. 线程A:删除缓存,准备更新数据库

  2. 线程B:查询数据,缓存未命中,读取数据库旧数据,回填缓存

  3. 线程A:完成数据库更新

最终结果:数据库是新数据,缓存被线程B回填旧数据,数据永久不一致!

2.4 错误方案总结

以上四种方案生产环境一律禁止使用,全部存在无法规避的并发一致性问题。

三、主流最优解决方案(生产常用)

接下来讲解生产环境主流、稳定、可落地的4种解决方案,从简单到进阶,适配不同一致性需求!

3.1 基础方案:Cache Aside 旁路缓存模式(先更库、后删缓存)

3.1.1 核心流程(最标准主流)

读流程

  1. 查询Redis缓存,命中直接返回

  2. 未命中,查询MySQL数据库

  3. 将数据库数据回填Redis,返回结果

写流程

  1. 优先更新MySQL数据库(保证数据源准确)

  2. 数据库更新成功后,删除Redis缓存(而非更新)

3.1.2 为什么是删除缓存,不是更新缓存?
  • 避免无效更新:频繁更新但无人读取的数据,无需频繁更新缓存,删除后下次查询自动回填最新数据

  • 避免并发覆盖:彻底解决多线程更新缓存互相覆盖的问题

3.1.3 存在的极小概率问题

仅存在一种极端不一致场景:读线程查询缓存未命中,读取旧数据库数据,此时写线程更新库+删缓存,随后读线程回填旧缓存

该场景发生概率极低,且可以通过缓存过期机制自愈,普通业务完全够用。

3.1.4 优缺点与适用场景

优点:实现简单、性能高、无多余开销、适配绝大多数业务

缺点:极端并发下存在短暂脏数据,依赖缓存过期自愈

适用场景:普通业务、对一致性要求不极致、高并发读写场景(90%互联网项目首选)

3.2 进阶方案:延迟双删策略(解决极端脏数据)

3.2.1 核心原理

先更库、后删缓存 的基础上,增加延时二次删除缓存,彻底解决极端并发脏数据问题。

3.2.2 完整执行流程
  1. 更新MySQL数据库

  2. 第一次删除Redis缓存

  3. 延时等待500ms~2s(大于一次读库+回填缓存的耗时)

  4. 第二次删除Redis缓存,清除可能被回填的旧数据

3.2.3 延时时间如何设置?

生产环境禁止写死时间 ,通过压测获取业务读请求最大耗时,延时略大于该耗时即可,常规设置500ms-2s。同时采用异步线程执行延时删除,不阻塞主线程,不影响接口性能。

3.2.4 优缺点与适用场景

优点:彻底解决极端并发脏数据问题,一致性大幅提升,实现简单

缺点:少量延时开销,无法做到实时强一致

适用场景:一致性要求较高、并发读写频繁的业务(商品信息、用户资料)

3.3 高阶方案:分布式锁(实现实时强一致性)

3.3.1 核心思想

通过分布式锁 ,锁住同一数据的读写、更新操作,保证更新数据库+删除缓存的原子性,杜绝并发穿插问题。

3.3.2 执行流程
  1. 写请求:获取当前数据的分布式锁 → 更新数据库 → 删除缓存 → 释放锁

  2. 读请求:获取读锁(读写互斥、读读共享)→ 查询缓存/数据库 → 释放锁

3.3.3 优缺点与适用场景

优点 :真正实现实时强一致性,无脏数据、无延时

缺点:加锁会降低接口吞吐量,存在锁竞争开销,实现复杂度高

适用场景:一致性要求极高的核心业务(库存、订单、余额、交易数据)

3.4 终极方案:Binlog异步同步(Canal 最终一致性)

3.4.1 核心原理

彻底解耦业务代码,不依赖业务逻辑操作缓存,通过Canal监听MySQL Binlog日志,异步感知数据库数据变更,自动删除/更新缓存。

3.4.2 完整执行链路

MySQL数据更新 → 记录Binlog日志 → Canal监听日志变更 → 推送变更数据 → MQ消息队列 → 消费者异步删除/更新Redis缓存

3.4.3 核心优势
  • 解耦业务:无需在业务代码中写任何缓存操作逻辑,减少代码入侵

  • 容错性极强:业务更新成功即可,缓存同步失败可重试,不会影响主流程

  • 彻底解决一致性:以数据库数据为唯一基准,异步兜底同步

  • 性能极高:主业务无任何缓存操作开销,异步串行同步,无并发冲突

3.4.4 优缺点与适用场景

优点:最终一致性最强、性能最优、业务无侵入、支持重试兜底

缺点:存在毫秒级异步延时,无法实时一致,部署复杂度高(需搭建Canal、MQ)

适用场景:绝大多数高并发生产业务、大数据量更新业务、核心兜底方案

四、所有方案对比+生产选型指南

解决方案 一致性级别 性能开销 实现难度 适用场景
Cache Aside基础版 弱一致(可自愈) 极低 极简 普通非核心业务
延迟双删 高一致(近实时) 低(异步延时) 简单 高频读写、较高一致性业务
分布式锁 实时强一致 较高(锁竞争) 较高 订单、库存、余额核心业务
Binlog+Canal异步同步 最终强一致 极低(异步解耦) 大型高并发项目、全局兜底

五、生产最佳实践(重点总结)

  1. 禁止使用四种错误双写方案,一律采用「先更库、后删缓存」为基础规范

  2. 普通业务:Cache Aside模式 + 缓存过期兜底,足够使用

  3. 中高一致性业务:Cache Aside + 异步延迟双删

  4. 核心金融/交易业务:分布式锁保证实时强一致

  5. 大型高并发项目:业务层延迟双删 + Canal Binlog异步兜底,双重保障,万无一失

  6. 通用兜底机制:所有缓存必须设置过期时间,即使出现脏数据,也能自动恢复,避免永久不一致

六、面试高频问答总结

Q1:为什么不推荐先更新缓存,而是删除缓存?

  1. 避免无效更新:无人读取的更新操作纯属性能浪费;2. 避免并发缓存覆盖问题,彻底杜绝多线程更新缓存导致的数据错乱。

Q2:延迟双删的延时时间怎么设置?

无需固定值,根据业务压测结果设置,延时时间略大于业务「读库+回填缓存」的最大耗时,生产常规500ms-2s,且必须异步执行不阻塞主线程。

Q3:Binlog方案的核心优势是什么?

业务无侵入、主流程无性能损耗、容错性高、支持失败重试,以数据库为唯一数据源头,从根本上保证最终一致性,是大型互联网公司的终极解决方案。

Q4:如何彻底杜绝缓存数据库不一致?

无绝对完美方案,实时强一致用分布式锁,最终强一致用Binlog异步同步,生产最优组合:业务层主动删缓存 + 延迟双删兜底 + Binlog异步兜底 + 缓存过期自愈

七、结尾

缓存与数据库双写一致性,没有万能方案,脱离业务场景谈方案都是空谈

本文从问题根源、错误避坑、逐级解决方案、生产选型、面试问答全方位覆盖,完全满足日常开发、面试、生产落地需求。大家可以根据自己项目的并发量、一致性要求、成本预算灵活选型!

码字不易,欢迎点赞、收藏、关注,后续持续更新后端高并发核心干货!

相关推荐
我也不曾来过16 小时前
MYSQL 使用C语言链接
数据库·mysql
SimonKing6 小时前
裁员、降薪潮来了,你被波及了么?
java·后端·程序员
倔强的石头_6 小时前
内核代差揭秘:从 DISTINCT 优化实测看国产数据库的逻辑推理深度
数据库
云边有个稻草人6 小时前
金仓数据库 KES:DISTINCT 语句性能优化实践与内核实现
数据库·金仓·kes·数据库内核优化·kes 数据库性能优化·distinct 语句优化·sql 调优
xuhaoyu_cpp_java6 小时前
Git学习(六)
git·学习
TheWolfsfaith6 小时前
Redis服务键控建通知安装
数据库·redis·缓存
装不满的克莱因瓶6 小时前
新版AI开发框架SpringAIAlibaba vs AgentScope 选型指南
java·开发语言·人工智能·ai·agent·alibaba·springai
凯瑟琳.奥古斯特7 小时前
原码与补码乘法符号位处理差异
java·开发语言·职场和发展
iiiiyu7 小时前
面向对象案例
java·大数据·开发语言·数据结构·python·编程语言