缓存一致性难题:Redis 和数据库如何做到数据一致?

做后端开发的小伙伴,几乎没人能绕开 Redis 缓存 + 数据库的组合。 我们用缓存的原因很简单:扛住高并发、减少数据库压力、提升接口响应速度。

但是使用缓存有一个躲不开的痛点:缓存和数据库的数据一致性问题;

经常出现这种情况:数据库的数据已经更新了,但是用户查询的时候却读到了 Redis 里的旧数据;

一、为什么会出现数据不一致问题呢?

1.1 核心原因:操作无法做到原子性

Redis 是内存存储,读写性能极高;MySQL 等关系型数据库是磁盘存储,事务执行耗时相对更长。二者是完全独立的两个存储组件,更新、删除操作无法保证原子性------ 既无法做到同时成功,也无法做到同时失败。只要其中一步成功、一步失败,就必然出现数据不一致。

1.2 三大典型不一致的场景

所有线上的一致性问题,大致可以归为以下三类:

  1. 更新数据库成功、操作缓存失败:是先更库后删缓存方案的主要风险,后果是短暂读旧数据,可通过过期时间自愈。
  2. **写入缓存成功、更新数据库失败:**是先更缓存后更库的致命问题,后果是永久脏数据,无自愈机制。
  3. **并发读写时序冲突:**是所有缓存方案都可能遇到的并发问题,需通过延时双删等机制规避。

二、应该先更新数据库还是先更新redis缓存呢?

2.1 必须禁止这种顺序:先更新缓存,后更新数据库

这个方案存在两个致命缺陷:

  • 数据库更新失败导致反向脏数据:如果缓存更新成功,但数据库更新失败(如事务回滚、异常宕机),此时缓存中是新数据,数据库是旧数据,且后续没有自动修正的机制,脏数据会一直存在。
  • 高并发下数据永久错乱:
  1. 缓存层面:A 先改、B 后改 ,缓存最后保留 B 的数据。
  2. 数据库层面:B 先入库、A 后入库 ,数据库最后被 A 覆盖。
  3. 结果:数据库是 A 版本,缓存是 B 版本,两者永久不一致,问题隐蔽且难以排查修复。

2.2 建议做法:先更新数据库,后操作缓存

所有业内成熟的缓存一致性方案,都基于这一核心原则:数据库是唯一的权威数据源,缓存只是数据副本,永远以数据库为准。

缓存的核心价值是「加速读取」,而非「存储数据」,绝对不能让缓存数据的优先级高于数据库。先操作数据库,再操作缓存,即使后续缓存操作失败,也只是短暂有效期内返回旧数据,最终可以通过缓存过期机制自愈,风险可控。

三、一致性的方案:基础与进阶

3.1 基础方案:更新数据库 + 删除redis缓存

别称:**懒加载模式**,读的时候才回填缓存;写操作不主动写缓存,而是删除缓存。

这是最通用、最基础的方案,也是业界标准的Cache Aside Pattern(旁路缓存模式)

  • 避免无效写入:业务中很多数据更新后,并不会被立即查询。如果每次更新都同步刷新缓存,会产生大量无效的缓存写入,浪费 Redis 性能;而删除缓存后,只有当下一次查询请求到来时,才会从数据库加载最新数据并回写。
  • 降低并发风险:更新缓存需要计算最新值,并发场景下容易出现旧值覆盖新值的问题;删除缓存是一个幂等操作,无论执行多少次结果都一致,逻辑更简单、安全性更高。
执行流程

读流程:

  1. 先查询 Redis 缓存,如果命中则直接返回数据;
  2. 如果缓存未命中,查询数据库获取最新数据;
  3. 将查询数据库的最新数据写入 Redis 缓存,再返回结果;

写流程:

  1. 业务层更新数据库数据;
  2. 数据库事务提交成功后,删除对应的 Redis 缓存键;
  3. 后续查询走"读流程":查询缓存未命中,自动从数据库加载新数据并回写到缓存;
优缺点与适用场景
  • ✅ 优点:逻辑简单、代码量少、性能损耗低,适配绝大多数业务场景
  • ❌ 缺点:存在短暂的不一致时间窗口;极端高并发读写场景下,可能出现「旧数据回写覆盖」的并发问题
  • 🎯 适用场景:普通后台业务、低并发读写场景、非核心非金融类数据(如配置信息、公告内容、普通用户资料)

3.2 进阶方案:延时双删方案

针对基础方案在高并发下的「旧数据回写」漏洞,业内的优化方案是延时双删,可以大幅降低并发读写冲突导致的不一致概率。

问题复现描述(基础方案的并发漏洞)

前提:缓存恰好过期 / 失效(或首次查询无缓存)

  1. 线程 A(读请求)查询缓存未命中,查询数据库,获取旧数据 v1;
  2. 线程 B(写请求)更新数据库为 v2,提交事务后执行删除缓存;
  3. 线程 B 完成缓存删除;
  4. 线程 A 将拿到的旧数据 v1 回写到 Redis 缓存。

结果:数据库为 v2,缓存写入旧数据 v1,产生脏缓存。脏数据会一直存在,直到缓存 TTL 过期,或下一次数据更新触发缓存删除。

解决方案:
  1. 更新数据库数据,提交事务
  2. 立即删除对应 Redis 缓存(第一次删除)
  3. 异步延迟一段时间(通常为 100~500ms,根据业务读请求耗时调整)
  4. 再次删除对应 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 异步删缓存 + 延时双删 + 定时过期 解耦 + 并发防护 + 多层兜底 多服务共享中台数据:商品中心、多端商家资料、内容中台文章,隔离缓存故障
强一致性核心业务 放弃缓存,直查数据库 绝对数据一致 资金余额、支付订单状态、实时库存扣减
相关推荐
SelectDB2 小时前
Doris VARIANT 速查:类型映射、建表写法、索引配置与常见注意点
大数据·数据库·数据分析
l1t2 小时前
几种数据库软件count(distinct)和count()from group by子查询性能测试
开发语言·数据库
夜雪一千2 小时前
如何在MySQL中实现字段级加密
android·数据库·mysql
CJi0NG2 小时前
【自用】MySQL-存储引擎
数据库·mysql
weixin_460443562 小时前
在线考试人脸识别如何合规?本地活体检测、人脸模板加密与替代认证的技术架构设计
大数据·数据库·煤矿培训考试系统·煤矿在线考试系统·煤矿安全培训·煤矿考试系统
deepseek232 小时前
SQLite WAL 开了仍报 database is locked?Python 后台任务的事务与幂等修复
数据库·python·sqlite·并发编程·wal
SelectDB3 小时前
冷热分层速查手册:参数、命令、生效判据与常见失效原因一页看完
大数据·数据库·数据分析
雨落在了我的手上3 小时前
MySQL数据库基础(9):数据库设计
数据库·mysql