大家好,今天我们来彻底吃透后端高频核心问题:缓存与数据库双写数据不一致!
这是面试必问、生产必踩坑的经典问题,几乎所有高并发项目(电商、秒杀、资讯、用户中心)都会遇到。本文从零拆解问题根源,手把手分析所有解决方案的优缺点、适用场景,附带清晰流程图、避坑要点,新手也能完全看懂,文末附生产选型总结!
一、前言:为什么会出现双写不一致?
1.1 缓存+数据库架构初衷
在后端开发中,为了提升查询性能、降低数据库压力 ,我们都会采用 MySQL + Redis 架构:
-
读请求:优先查Redis缓存,命中直接返回,未命中查询MySQL,再回填缓存
-
写请求:更新数据时,需要同时操作MySQL和Redis
但问题随之而来:数据库和缓存是两个独立的存储组件,无法做到原子性同时更新 ,一旦两次操作执行顺序错乱、并发冲突、操作失败,就会出现缓存存旧数据、数据库存新数据的双写不一致问题。
1.2 核心不一致场景
所有不一致问题,都源于并发读写冲突 和操作非原子性,典型场景:
-
线程A更新数据库,还没来得及操作缓存,线程B读取旧缓存数据
-
先删缓存、后更新数据库的间隙,有请求回填旧缓存
-
更新数据库成功,但缓存删除/更新失败,永久脏数据
-
高并发读写穿插执行,打乱双写顺序
二、四种错误双写方案(避坑重点)
很多新手会写出四种错误的双写逻辑,看似没问题,实则高并发必崩,下面逐一拆解!
2.1 方案一:先更新缓存,再更新数据库
执行流程:更新Redis → 更新MySQL
致命问题:数据丢失+永久不一致
如果缓存更新成功,数据库更新失败,缓存是新数据,数据库是旧数据,后续所有读请求都会读到错误缓存数据,数据库永久无法同步。
2.2 方案二:先更新数据库,再更新缓存
执行流程:更新MySQL → 更新Redis
两大致命问题:
-
并发覆盖问题 :高并发下,两个更新请求穿插执行 线程A(更新旧值10→20)、线程B(更新10→30) A更新数据库成功,B更新数据库成功 B先更新缓存为30,A后更新缓存为20 最终数据库30,缓存20,永久脏数据
-
无效更新开销:频繁更新但无人查询的数据,反复更新缓存纯属浪费性能
2.3 方案三:先删除缓存,再更新数据库
执行流程:删除Redis → 更新MySQL
核心漏洞:读写并发冲突,百分百出现不一致
并发场景复现:
-
线程A:删除缓存,准备更新数据库
-
线程B:查询数据,缓存未命中,读取数据库旧数据,回填缓存
-
线程A:完成数据库更新
最终结果:数据库是新数据,缓存被线程B回填旧数据,数据永久不一致!
2.4 错误方案总结
以上四种方案生产环境一律禁止使用,全部存在无法规避的并发一致性问题。
三、主流最优解决方案(生产常用)
接下来讲解生产环境主流、稳定、可落地的4种解决方案,从简单到进阶,适配不同一致性需求!
3.1 基础方案:Cache Aside 旁路缓存模式(先更库、后删缓存)
3.1.1 核心流程(最标准主流)
读流程:
-
查询Redis缓存,命中直接返回
-
未命中,查询MySQL数据库
-
将数据库数据回填Redis,返回结果
写流程:
-
优先更新MySQL数据库(保证数据源准确)
-
数据库更新成功后,删除Redis缓存(而非更新)
3.1.2 为什么是删除缓存,不是更新缓存?
-
避免无效更新:频繁更新但无人读取的数据,无需频繁更新缓存,删除后下次查询自动回填最新数据
-
避免并发覆盖:彻底解决多线程更新缓存互相覆盖的问题
3.1.3 存在的极小概率问题
仅存在一种极端不一致场景:读线程查询缓存未命中,读取旧数据库数据,此时写线程更新库+删缓存,随后读线程回填旧缓存。
该场景发生概率极低,且可以通过缓存过期机制自愈,普通业务完全够用。
3.1.4 优缺点与适用场景
优点:实现简单、性能高、无多余开销、适配绝大多数业务
缺点:极端并发下存在短暂脏数据,依赖缓存过期自愈
适用场景:普通业务、对一致性要求不极致、高并发读写场景(90%互联网项目首选)
3.2 进阶方案:延迟双删策略(解决极端脏数据)
3.2.1 核心原理
在 先更库、后删缓存 的基础上,增加延时二次删除缓存,彻底解决极端并发脏数据问题。
3.2.2 完整执行流程
-
更新MySQL数据库
-
第一次删除Redis缓存
-
延时等待500ms~2s(大于一次读库+回填缓存的耗时)
-
第二次删除Redis缓存,清除可能被回填的旧数据
3.2.3 延时时间如何设置?
生产环境禁止写死时间 ,通过压测获取业务读请求最大耗时,延时略大于该耗时即可,常规设置500ms-2s。同时采用异步线程执行延时删除,不阻塞主线程,不影响接口性能。
3.2.4 优缺点与适用场景
优点:彻底解决极端并发脏数据问题,一致性大幅提升,实现简单
缺点:少量延时开销,无法做到实时强一致
适用场景:一致性要求较高、并发读写频繁的业务(商品信息、用户资料)
3.3 高阶方案:分布式锁(实现实时强一致性)
3.3.1 核心思想
通过分布式锁 ,锁住同一数据的读写、更新操作,保证更新数据库+删除缓存的原子性,杜绝并发穿插问题。
3.3.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异步同步 | 最终强一致 | 极低(异步解耦) | 高 | 大型高并发项目、全局兜底 |
五、生产最佳实践(重点总结)
-
禁止使用四种错误双写方案,一律采用「先更库、后删缓存」为基础规范
-
普通业务:Cache Aside模式 + 缓存过期兜底,足够使用
-
中高一致性业务:Cache Aside + 异步延迟双删
-
核心金融/交易业务:分布式锁保证实时强一致
-
大型高并发项目:业务层延迟双删 + Canal Binlog异步兜底,双重保障,万无一失
-
通用兜底机制:所有缓存必须设置过期时间,即使出现脏数据,也能自动恢复,避免永久不一致
六、面试高频问答总结
Q1:为什么不推荐先更新缓存,而是删除缓存?
- 避免无效更新:无人读取的更新操作纯属性能浪费;2. 避免并发缓存覆盖问题,彻底杜绝多线程更新缓存导致的数据错乱。
Q2:延迟双删的延时时间怎么设置?
无需固定值,根据业务压测结果设置,延时时间略大于业务「读库+回填缓存」的最大耗时,生产常规500ms-2s,且必须异步执行不阻塞主线程。
Q3:Binlog方案的核心优势是什么?
业务无侵入、主流程无性能损耗、容错性高、支持失败重试,以数据库为唯一数据源头,从根本上保证最终一致性,是大型互联网公司的终极解决方案。
Q4:如何彻底杜绝缓存数据库不一致?
无绝对完美方案,实时强一致用分布式锁,最终强一致用Binlog异步同步,生产最优组合:业务层主动删缓存 + 延迟双删兜底 + Binlog异步兜底 + 缓存过期自愈。
七、结尾
缓存与数据库双写一致性,没有万能方案,脱离业务场景谈方案都是空谈。
本文从问题根源、错误避坑、逐级解决方案、生产选型、面试问答全方位覆盖,完全满足日常开发、面试、生产落地需求。大家可以根据自己项目的并发量、一致性要求、成本预算灵活选型!
码字不易,欢迎点赞、收藏、关注,后续持续更新后端高并发核心干货!