news 2026/5/31 13:14:12

缓存一致性难题破解:Redis如何保证缓存与数据库的数据一致性?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
缓存一致性难题破解:Redis如何保证缓存与数据库的数据一致性?

缓存一致性难题破解:Redis如何保证缓存与数据库的数据一致性?

    • 前言
    • 一、问题是怎么产生的?
      • 1.1 理想情况
      • 1.2 问题场景
      • 1.3 常见错误做法
    • 二、三大经典策略对比
      • 策略一:Cache Aside Pattern(旁路缓存)—— **最推荐**
      • 策略二:Read/Write Through(读写穿透)
      • 策略三:Write Behind(写回)
      • 策略对比总结
    • 三、Cache Aside 的并发问题与解决方案
      • 3.1 问题场景:先删缓存 vs 后删缓存
      • 3.2 方案 B:先更新数据库,再删缓存(✅ 推荐)
      • 3.3 极端并发:二次删除(延迟双删)
    • 四、终极方案:订阅 MySQL Binlog(Canal)
      • 4.1 原理架构
      • 4.2 核心步骤
      • 4.3 优点
      • 4.4 快速上手示例
    • 五、各种方案的最终一致性对比
    • 六、生产环境最佳实践
      • 6.1 标准写流程
      • 6.2 缓存穿透、雪崩、击穿的应对
      • 6.3 重试机制保障缓存删除成功
    • 七、常见面试题
      • Q1:为什么是删除缓存而不是更新缓存?
      • Q2:先更新数据库还是先删除缓存?
      • Q3:缓存删除失败怎么办?
      • Q4:Canal 方案会不会有延迟?
      • Q5:如果读请求很强,删除缓存瞬间又有大量读怎么办?
    • 总结

🌺The Begin🌺点点关注,收藏不迷路🌺

⬇ ⬇ 底部 ⬇ ⬇

前言

在互联网后端架构中,Redis 作为高性能缓存被广泛应用。但一个经典难题始终困扰着开发者:

数据库中的数据更新后,如何保证 Redis 缓存中的数据也是最新的?

这就是缓存与数据库一致性问题。

如果处理不当,会导致:

  • 用户看到脏数据
  • 库存数据错误引发超卖
  • 订单状态混乱

今天这篇文章,我们从问题根源 → 三大经典策略 → 最终一致性方案 → 生产实践,彻底讲透缓存一致性问题。


一、问题是怎么产生的?

1.1 理想情况

用户请求 → 读缓存 → 命中 → 返回 ↓ 未命中 → 读数据库 → 写缓存 → 返回

1.2 问题场景

写操作发生时,缓存和数据库可能不一致:

时间线: 1. 线程 A 更新数据库:将商品价格从 100 改为 80 2. 线程 B 读缓存:仍读到旧的 100(脏数据) 3. 线程 A 删除缓存(如果采用删除策略)

问题根源:更新数据库和操作缓存是两个独立的操作,无法保证原子性

1.3 常见错误做法

做法问题
先更新数据库,再更新缓存并发写导致缓存脏数据
先更新缓存,再更新数据库缓存成功,数据库失败 → 永久不一致
先删除缓存,再更新数据库删除后、更新前,其他线程读到旧数据并回写缓存
先更新数据库,再删除缓存删除失败 → 缓存永久为脏数据

二、三大经典策略对比

策略一:Cache Aside Pattern(旁路缓存)——最推荐

读流程:

┌─────────┐ │ 读请求 │ └────┬────┘ ▼ ┌─────────┐ │ 查缓存 │ └────┬────┘ 命中 / \ 未命中 ▼ ▼ 返回数据 查数据库 │ ▼ 写缓存 │ ▼ 返回数据

写流程:

┌─────────┐ │ 写请求 │ └────┬────┘ ▼ ┌─────────┐ │ 更新数据库│ └────┬────┘ ▼ ┌─────────┐ │ 删除缓存 │ └─────────┘

核心:先更新数据库,再删除缓存。

为什么是删除而不是更新缓存?

  • 更新缓存需要知道新值,可能需要复杂计算
  • 删除后等下次读时再加载,懒加载更简单

策略二:Read/Write Through(读写穿透)

缓存作为唯一数据源,应用只和缓存交互,缓存负责与数据库同步。

写请求 → 缓存 → 数据库 读请求 → 缓存 → 数据库(未命中时)

优点:应用层无感知
缺点:缓存层实现复杂

策略三:Write Behind(写回)

先写缓存,异步批量写数据库。

优点:写入性能最高
缺点:数据可能丢失(缓存宕机)

适用场景:高并发写入,对一致性要求不高(如点赞数、浏览量)

策略对比总结

策略一致性性能复杂度适用场景
Cache Aside大多数业务(推荐)
Read/Write Through需要缓存抽象层
Write Behind最高日志、计数类

三、Cache Aside 的并发问题与解决方案

3.1 问题场景:先删缓存 vs 后删缓存

方案 A:先删缓存,再更新数据库(❌ 不推荐)

时间线: 线程 A(写) 线程 B(读) 1. 删除缓存 2. 读缓存 → 未命中 3. 读数据库(旧值) 4. 写缓存(旧值) 5. 更新数据库(新值)

结果:缓存中是旧值,数据库是新值 →不一致

3.2 方案 B:先更新数据库,再删缓存(✅ 推荐)

时间线: 线程 A(写) 线程 B(读) 1. 更新数据库(新值) 2. 读缓存(旧值命中) 3. 删除缓存

问题:步骤 2 读到的是旧值,但仅持续几十毫秒,下次读就正常了。这是最终一致性,绝大多数业务可接受。

3.3 极端并发:二次删除(延迟双删)

针对极小概率的异常场景(读线程在写线程删除缓存前写入了旧数据):

publicvoidupdate(Stringkey,ObjectnewValue){// 1. 第一次删除缓存redis.del(key);// 2. 更新数据库db.update(newValue);// 3. 休眠一段时间(比读操作耗时稍长)Thread.sleep(500);// 4. 第二次删除缓存redis.del(key);}

为什么有效:确保步骤 3 期间任何回写缓存的旧数据都被第二次删除清除。

缺点:引入固定延迟,影响写性能。


四、终极方案:订阅 MySQL Binlog(Canal)

4.1 原理架构

┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 业务应用 │ → │ MySQL │ → │ Binlog │ → │ Canal │ └─────────┘ └────┬────┘ └─────────┘ └────┬────┘ │ │ │ ▼ ┌─────┴─────┐ ┌─────────┐ │ 数据写入 │ │ 消息队列 │ └───────────┘ └────┬────┘ │ ▼ ┌─────────┐ │ 消费者 │ └────┬────┘ │ ▼ ┌─────────┐ │ 删除缓存 │ └─────────┘

4.2 核心步骤

  1. 业务应用:只更新数据库,不操作缓存
  2. MySQL:生成 Binlog
  3. Canal:伪装成 MySQL Slave,拉取 Binlog
  4. 消息队列:Canal 将变更事件发送到 MQ
  5. 消费者:收到事件后删除对应缓存

4.3 优点

优势说明
彻底解耦业务代码无需关心缓存
保证一致性只要 Binlog 被消费,缓存一定被删除
支持异构可同时更新 ES、HBase 等其他存储

4.4 快速上手示例

启动 Canal Server(docker-compose)

version:'3'services:canal:image:canal/canal-server:v1.1.6environment:canal.instance.master.address:mysql:3306canal.instance.dbUsername:canalcanal.instance.dbPassword:canalcanal.instance.filter.regex:mydb\\..*ports:-"11111:11111"

Java 客户端消费

@CanalEventListenerpublicclassCacheSyncListener{@InsertListenPoint@DeleteListenPoint@UpdateListenPointpublicvoidonEvent(CanalEntry.Entryentry){StringtableName=entry.getHeader().getTableName();// 解析出变更的主键 IDStringid=parseId(entry);// 删除缓存redis.del(tableName+":"+id);}}

五、各种方案的最终一致性对比

方案不一致窗口实现复杂度是否侵入业务代码推荐度
先删缓存,再更新 DB几百毫秒~几秒⭐⭐
先更新 DB,再删缓存几十毫秒⭐⭐⭐⭐
延迟双删500ms 固定延迟⭐⭐⭐
Binlog 订阅(Canal)毫秒级⭐⭐⭐⭐⭐
设置缓存过期时间过期前都不一致⭐⭐(兜底)

结论

  • 一般业务:先更新 DB,再删缓存 + 设置合理过期时间(如 30 秒)—— 简单够用
  • 一致性要求高:Canal + 消息队列方案 —— 彻底解耦,可靠

六、生产环境最佳实践

6.1 标准写流程

@ServicepublicclassProductService{@AutowiredprivateRedisTemplateredis;@AutowiredprivateProductDaoproductDao;@TransactionalpublicvoidupdateProduct(Productproduct){// 1. 更新数据库productDao.updateById(product);// 2. 删除缓存(而非更新)StringcacheKey="product:"+product.getId();redis.delete(cacheKey);// 3. 可选:发送 MQ 消息,异步删除其他关联缓存mq.send("cache.delete",cacheKey);}publicProductgetProduct(Longid){StringcacheKey="product:"+id;// 1. 查缓存Productproduct=(Product)redis.opsForValue().get(cacheKey);if(product!=null){returnproduct;}// 2. 缓存未命中,查数据库product=productDao.selectById(id);if(product!=null){// 3. 写缓存,设置合理过期时间redis.opsForValue().set(cacheKey,product,300,TimeUnit.SECONDS);}returnproduct;}}

6.2 缓存穿透、雪崩、击穿的应对

问题解决方案
缓存穿透(查询不存在的数据)布隆过滤器 / 缓存空对象(过期时间短)
缓存雪崩(大量缓存同时过期)过期时间加随机值 / 多级缓存
缓存击穿(热点 key 过期瞬间)互斥锁 / 逻辑过期

6.3 重试机制保障缓存删除成功

publicvoiddeleteCacheWithRetry(Stringkey,intmaxRetries){for(inti=0;i<maxRetries;i++){try{redis.del(key);return;}catch(Exceptione){// 重试前短暂等待Thread.sleep(100*(i+1));}}// 最终失败 → 发送告警 + 写入重试队列alarm.send("缓存删除失败: "+key);retryQueue.offer(key);}

七、常见面试题

Q1:为什么是删除缓存而不是更新缓存?

  • 更新缓存需要知道新值,可能涉及复杂计算
  • 如果缓存被多个线程频繁更新,浪费计算资源
  • 懒加载(删除后下次读时加载)更简单可靠

Q2:先更新数据库还是先删除缓存?

先更新数据库,再删除缓存。原因:

  • 先删缓存再更新 DB,并发读可能回写旧数据
  • 先更新 DB 再删缓存,不一致窗口更短

Q3:缓存删除失败怎么办?

  • 增加重试机制(3-5 次)
  • 失败后写入消息队列,异步重试
  • 设置兜底的过期时间(如 30 秒),即使删除失败也会自动失效

Q4:Canal 方案会不会有延迟?

Binlog 消费延迟通常在毫秒级,比业务代码主动删除缓存多了网络 + 消费开销(10-50ms),但换来的是业务代码零侵入和更高的可靠性。

Q5:如果读请求很强,删除缓存瞬间又有大量读怎么办?

这就是缓存击穿。解决方案:

// 互斥锁,只允许一个线程更新缓存synchronized(key.intern()){// 双重检查if(redis.get(key)==null){dbData=db.query();redis.set(key,dbData);}}

总结

核心结论说明
首选策略Cache Aside:先更新数据库,再删除缓存
兜底保障缓存设置合理过期时间(如 30 秒~5 分钟)
终极方案Canal 订阅 Binlog,异步删除缓存
并发优化延迟双删(应对极端情况)
失败处理重试 + MQ 异步队列
性能防护互斥锁防击穿,随机过期防雪崩

最终一句话:

99% 的场景:先更新 DB + 后删缓存 + 设置过期时间 = 简单够用
极致一致性:Canal 监听 Binlog + 异步删除 = 彻底解耦


🌺The End🌺点点关注,收藏不迷路🌺

⬆ ⬆ 顶部 ⬆ ⬆
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/31 13:13:41

5分钟掌握Windows界面定制:用ExplorerPatcher打造个性化工作环境

5分钟掌握Windows界面定制&#xff1a;用ExplorerPatcher打造个性化工作环境 【免费下载链接】ExplorerPatcher This project aims to enhance the working environment on Windows 项目地址: https://gitcode.com/GitHub_Trending/ex/ExplorerPatcher 厌倦了Windows 11…

作者头像 李华
网站建设 2026/5/31 13:11:35

Arduino流水灯项目实战:从零打造9灯追逐灯光秀

1. 项目概述&#xff1a;从零打造一个动态LED灯光秀如果你刚接触Arduino或嵌入式开发&#xff0c;想找一个既有趣又能综合练习电路搭建和编程的项目&#xff0c;那么这个9灯追逐的LED灯光秀绝对是个完美的起点。它不像点亮单个LED那样简单&#xff0c;也不至于复杂到让人望而却…

作者头像 李华
网站建设 2026/5/31 13:10:14

Zig 2026 年开发日志:ELF 链接器、构建系统等多项更新,性能显著提升!

Zig 主分支近期更新资讯本页面精心整理了 Zig 主分支的近期更新内容&#xff0c;也可通过 RSS 订阅获取相关信息。本页面记录的是 2026 年的更新&#xff0c;其他年份的更新可在开发日志存档页面查看。2026 年 5 月 30 日&#xff1a;ELF 链接器改进Matthew Lugg 在过去几周改进…

作者头像 李华
网站建设 2026/5/31 13:08:34

3分钟掌握专业图片差异检测:PicQuickCompare图像比对工具终极指南

3分钟掌握专业图片差异检测&#xff1a;PicQuickCompare图像比对工具终极指南 【免费下载链接】PicQuickCompare Compare two pictures quickly 项目地址: https://gitcode.com/gh_mirrors/pi/PicQuickCompare 你是否曾为寻找两张图片之间的细微差别而烦恼&#xff1f;无…

作者头像 李华