做后端开发的小伙伴,几乎没人能绕开 Redis 缓存 + 数据库的组合。 我们用缓存的原因很简单:扛住高并发、减少数据库压力、提升接口响应速度。
但是使用缓存有一个躲不开的痛点:缓存和数据库的数据一致性问题;
经常出现这种情况:数据库的数据已经更新了,但是用户查询的时候却读到了 Redis 里的旧数据;
一、为什么会出现数据不一致问题呢?
1.1 核心原因:操作无法做到原子性
Redis 是内存存储,读写性能极高;MySQL 等关系型数据库是磁盘存储,事务执行耗时相对更长。二者是完全独立的两个存储组件,更新、删除操作无法保证原子性------ 既无法做到同时成功,也无法做到同时失败。只要其中一步成功、一步失败,就必然出现数据不一致。
1.2 三大典型不一致的场景
所有线上的一致性问题,大致可以归为以下三类:
- 更新数据库成功、操作缓存失败:是先更库后删缓存方案的主要风险,后果是短暂读旧数据,可通过过期时间自愈。
- **写入缓存成功、更新数据库失败:**是先更缓存后更库的致命问题,后果是永久脏数据,无自愈机制。
- **并发读写时序冲突:**是所有缓存方案都可能遇到的并发问题,需通过延时双删等机制规避。
二、应该先更新数据库还是先更新redis缓存呢?
2.1 必须禁止这种顺序:先更新缓存,后更新数据库
这个方案存在两个致命缺陷:
- 数据库更新失败导致反向脏数据:如果缓存更新成功,但数据库更新失败(如事务回滚、异常宕机),此时缓存中是新数据,数据库是旧数据,且后续没有自动修正的机制,脏数据会一直存在。
- 高并发下数据永久错乱:
- 缓存层面:A 先改、B 后改 ,缓存最后保留 B 的数据。
- 数据库层面:B 先入库、A 后入库 ,数据库最后被 A 覆盖。
- 结果:数据库是 A 版本,缓存是 B 版本,两者永久不一致,问题隐蔽且难以排查修复。
2.2 建议做法:先更新数据库,后操作缓存
所有业内成熟的缓存一致性方案,都基于这一核心原则:数据库是唯一的权威数据源,缓存只是数据副本,永远以数据库为准。
缓存的核心价值是「加速读取」,而非「存储数据」,绝对不能让缓存数据的优先级高于数据库。先操作数据库,再操作缓存,即使后续缓存操作失败,也只是短暂有效期内返回旧数据,最终可以通过缓存过期机制自愈,风险可控。
三、一致性的方案:基础与进阶
3.1 基础方案:更新数据库 + 删除redis缓存
别称:**懒加载模式**,读的时候才回填缓存;写操作不主动写缓存,而是删除缓存。
这是最通用、最基础的方案,也是业界标准的Cache Aside Pattern(旁路缓存模式)
- 避免无效写入:业务中很多数据更新后,并不会被立即查询。如果每次更新都同步刷新缓存,会产生大量无效的缓存写入,浪费 Redis 性能;而删除缓存后,只有当下一次查询请求到来时,才会从数据库加载最新数据并回写。
- 降低并发风险:更新缓存需要计算最新值,并发场景下容易出现旧值覆盖新值的问题;删除缓存是一个幂等操作,无论执行多少次结果都一致,逻辑更简单、安全性更高。
执行流程
读流程:
- 先查询 Redis 缓存,如果命中则直接返回数据;
- 如果缓存未命中,查询数据库获取最新数据;
- 将查询数据库的最新数据写入 Redis 缓存,再返回结果;
写流程:
- 业务层更新数据库数据;
- 数据库事务提交成功后,删除对应的 Redis 缓存键;
- 后续查询走"读流程":查询缓存未命中,自动从数据库加载新数据并回写到缓存;
优缺点与适用场景
- ✅ 优点:逻辑简单、代码量少、性能损耗低,适配绝大多数业务场景
- ❌ 缺点:存在短暂的不一致时间窗口;极端高并发读写场景下,可能出现「旧数据回写覆盖」的并发问题
- 🎯 适用场景:普通后台业务、低并发读写场景、非核心非金融类数据(如配置信息、公告内容、普通用户资料)
3.2 进阶方案:延时双删方案
针对基础方案在高并发下的「旧数据回写」漏洞,业内的优化方案是延时双删,可以大幅降低并发读写冲突导致的不一致概率。
问题复现描述(基础方案的并发漏洞)
前提:缓存恰好过期 / 失效(或首次查询无缓存)
- 线程 A(读请求)查询缓存未命中,查询数据库,获取旧数据 v1;
- 线程 B(写请求)更新数据库为 v2,提交事务后执行删除缓存;
- 线程 B 完成缓存删除;
- 线程 A 将拿到的旧数据 v1 回写到 Redis 缓存。
结果:数据库为 v2,缓存写入旧数据 v1,产生脏缓存。脏数据会一直存在,直到缓存 TTL 过期,或下一次数据更新触发缓存删除。
解决方案:
- 更新数据库数据,提交事务
- 立即删除对应 Redis 缓存(第一次删除)
- 异步延迟一段时间(通常为 100~500ms,根据业务读请求耗时调整)
- 再次删除对应 Redis 缓存(第二次删除)
第二次删除的作用,是清除并发场景下被旧数据覆盖的缓存,彻底堵上时序漏洞。
延时双删的延时时长建议取值为读接口 P99 耗时。P99 即 99 分位耗时,代表 99% 的读请求可在该时间内完成,仅 1% 请求超过该耗时;以此作为延迟时间,可覆盖绝大多数慢读回填场景,兼顾一致性与系统压力。
优缺点与适用场景
- ✅ 优点:解决高并发读写冲突,一致性精度大幅提升,实现成本低
- ❌ 缺点:需要开启异步线程执行延时删除;延时时长需要根据业务压测微调,无法适配所有极端时序
- 🎯 适用场景:高并发查询、频繁更新的核心业务数据(如商品信息、活动配置、库存展示)
3.3 兜底方案:过期失效 + 异步更新
没有任何实时方案能做到 100% 绝对一致 ------ 网络抖动、服务宕机、操作超时都可能导致缓存删除失败。因此必须有兜底机制,保证不一致的时长可控。
(1)定时过期:最终一致性的最后防线
所有 Redis 缓存都必须强制设置过期时间(TTL)。 哪怕出现极端异常导致缓存删除失败,只要过期时间一到,旧缓存就会自动失效,下一次查询会自动从数据库加载最新数据。过期时间越长,不一致的窗口期越长;过期时间越短,数据库压力越大,需要根据业务特性平衡。
(2)异步更新:解耦缓存操作,提升可靠性
对于超高并发的核心业务,可以引入消息队列(MQ)或数据库 binlog 订阅实现异步缓存操作:
- MQ 异步删缓存:业务完成数据库操作后,发送一条消息到 MQ,由消费者异步执行缓存删除 / 更新。既规避了同步操作的超时、失败问题,也不影响主业务的响应速度。
- Binlog 订阅同步:通过 Canal 等工具订阅数据库 binlog,感知数据变更后异步更新缓存。这种方式完全解耦业务代码,缓存操作与业务逻辑无侵入,适合多服务共享数据的场景。
优缺点与适用场景
- ✅ 优点:自带自愈能力,极端场景下数据不一致时长可控;异步方案不阻塞主流程,系统可用性更高
- ❌ 缺点:过期时间内存在不一致;异步方案增加架构复杂度,存在消息延迟、丢失的风险
- 🎯 适用场景:所有缓存场景的必备兜底;超高并发核心业务搭配异步方案使用
四、常见开发埋下的4个坑点
误区 1:缓存更新优先于数据库
再次强调:绝对不要先更新缓存、后更新数据库。数据库是唯一可信的数据源,缓存永远是辅助加速的副本,主次关系不能颠倒。
误区 2:不设置缓存过期时间
没有过期时间的「永久缓存」是线上重大隐患:一旦出现数据不一致,脏数据会永久存在,只能人工排查清理;同时也容易引发缓存雪崩、内存溢出等问题。
误区 3:高并发场景只用单次删缓存
单次删除缓存只在低并发场景下有效,高并发读写场景下大概率会出现旧数据覆盖问题,核心业务必须用延时双删 + 过期时间兜底。
误区 4:过度追求强一致性
缓存架构的本质是最终一致性 ,而非实时强一致性。它通过牺牲极短时间的数据一致性,换取系统的高性能和高可用性。 如果业务要求 100% 实时强一致(如金融资金核算、支付订单状态、库存扣减),最正确的做法是直接放弃缓存,直连数据库查询,不要强行用缓存方案硬套。
五、总结:整理业务场景对应推荐方案
| 业务场景 | 推荐方案组合 | 核心保障 | 典型业务示例 |
|---|---|---|---|
| 普通低并发业务 | 先更新数据库 + 删缓存 + 设置过期时间 | 基础一致性 + 兜底自愈 | 单体系统用户资料、平台公告、基础字典,读压力低 |
| 高并发读写业务 | 先更新数据库 + 延时双删 + 缓存过期兜底 | 解决并发冲突 + 兜底自愈 | 单服务维护热点数据:单体商品后台、独立博客文章,不引入 CDC |
| 核心超高并发业务 | 数据库事务 + MQ/binlog 异步删缓存 + 延时双删 + 定时过期 | 解耦 + 并发防护 + 多层兜底 | 多服务共享中台数据:商品中心、多端商家资料、内容中台文章,隔离缓存故障 |
| 强一致性核心业务 | 放弃缓存,直查数据库 | 绝对数据一致 | 资金余额、支付订单状态、实时库存扣减 |