当前位置: 首页 > news >正文

缓存一致性难题破解: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🌺点点关注,收藏不迷路🌺

⬆ ⬆ 顶部 ⬆ ⬆
http://www.jsqmd.com/news/923000/

相关文章:

  • 用Python从零实现Boids鸟群算法:游戏开发与数据可视化实战(附完整代码)
  • GetQzonehistory:5分钟永久备份QQ空间所有历史记忆
  • 重要文件分类归档技巧,助力长期安全保存 - 品牌测评鉴赏家
  • 百度网盘限速终结者:BaiduPCS-Web与KinhDown完整免费加速指南
  • DLSS Swapper终极指南:5分钟快速掌握游戏性能优化神器
  • Unity 2022.3 导出 OBJ 模型到 Blender 的完整避坑指南(含坐标系、材质修复)
  • 苏州路智通市政工程:太仓有实力的地下车库划线公司找哪家 - LYL仔仔
  • 别再乱选启动项了!Surface和旧电脑重装Windows 10/11,UEFI和Legacy设置保姆级指南
  • Switch上刷B站的终极方案:wiliwili完整安装与美化指南
  • 非可编程BSPD硬件设计:从LM393比较器到RC延时电路的赛车安全系统实现
  • 量子梯度估计中的LCU方法:原理与应用
  • Windows 10 OneDrive 彻底卸载解决方案:专业级系统清理指南
  • 跨平台网络资源下载神器:3分钟快速上手res-downloader完整指南
  • 抖音视频保存到相册失败怎么办?2026常见问题+解决方法 - 科技大爆炸
  • 网盘直链解析工具:打破下载速度限制的9大平台解决方案
  • 2026石家庄莫奈闲置变现指南——收的顶,让你的包包高价安心出手 - 奢侈品回收测评
  • 基于Arduino与ESP8266的智能灌溉系统:从传感器到手机控制的完整实践
  • 全屋定制哪家好?RERA源木匠心为你打造品质生活 - 产品测评官
  • 2026年宿州市CPPM报名十大核心问题全流程答疑 - 众智商学院课程中心
  • 告别双边滤波的卡顿:用OpenCV的guidedFilter函数实现图像快速保边平滑(Python实战)
  • AVR ISP通用编程适配器设计:兼容多型号ATTiny芯片的硬件解决方案
  • 网络通信基石:TCP三次握手的完整剖析
  • 基于BeagleBone Black与BLE 5.0的物联网设备开发实践
  • 易拉罐DIY AM天线:从材料替代到信号增强的无线电实践
  • 如何用BiRefNet实现高精度图像分割:新手完整指南
  • 别再为WVP-PRO和ZLM重启循环头疼了!一个配置修改搞定服务稳定连接
  • 基于Spring MVC的三角形测试系统设计与实现
  • 高三英语只有70分还有救吗?低分逆袭靠谱教育机构实测推荐 - 品牌测评鉴赏家
  • 为什么有些人表面嫌弃别人脏,自己家苍蝇满天飞的叮咬食物,也不嫌弃自己脏,为什么这样双标?
  • 物理服务器装CentOS 7.9,从BIOS设置到分区规划保姆级避坑指南