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

Redis与MySQL数据一致性问题的策略模式及解决方案

目录

一、策略模式

1、旁路缓存模式(Cache Aside Pattern)

2、读写穿透(Read-Through/Write-Through)

3、异步缓存写入(Write Behind)

二、一致性解决方案

1、缓存延迟双删

2、删除重试机制

3、读取biglog异步删除缓存

三、总结


在开发中,一般会使用Redis缓存一些常用的热点数据用来减少数据库IO,提高系统的吞吐量

先了解一下分布式系统中的一致性概念。

  • 强一致性:所有节点的数据必须实时同步,保证任何时候读取到的数据都是最新的。

  • 弱一致性:系统允许数据暂时不一致,但最终会达到一致状态。

  • 最终一致性:数据更新后,经过一段时间,系统会逐步达到一致状态。这个时间不固定,但在业务允许的范围内。

双写一致性:当数据同时存在于缓存(Redis)和数据库(MySQL)时,两者之间数据一致

 那么容易出现数据一致性问题的场景是:

  • 数据写入数据库,未更新缓存
  • 删除缓存后,数据库更新失败

一、策略模式

缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。有三种经典的缓存使用模式:

  • Cache-Aside Pattern

  • Read-Through/Write-through

  • Write-behind

1、旁路缓存模式(Cache Aside Pattern)

Cache Aside Pattern的提出是为了尽可能地解决缓存与数据库的数据不一致问题

流程:

  • 读取操作:先从缓存中读取数据,缓存命中返回结果;缓存未命中,从DB中读取数据,并将数据写入缓存。

  • 更新操作:先更DB,再删除缓存中的旧数据。

在日常开发中,一般使用了Cache Aside Pattern缓存更新策略模式,以数据库为主,缓存为辅

public class CacheAsidePattern {private RedisService redis;private DatabaseService database;// 读取操作public String getData(String key) {// 从缓存中获取数据String value = redis.get(key);if (value == null) {// 缓存未命中,从数据库获取数据value = database.get(key);if (value != null) {// 将数据写入缓存redis.set(key, value);}}return value;}// 更新操作public void updateData(String key, String value) {// 更新数据库database.update(key, value);// 删除缓存中的旧数据redis.delete(key);}
}

 ❓:Cache-Aside在操作数据库时,为什么是先操作数据库呢?为什么不先操作缓存呢?

1、先删除缓存后,数据库更新失败

🔺线程1:删除缓存A,由于网络问题没有操作数据库失败

🔻线程2:查询A,缓存无数据,并把A写入缓存

🔺线程1:网络堵塞结束,修改数据库A为B

那么此时缓存是A【旧数据】,数据库是B【新数据】,脏数据出现啦!!!

因此,Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存

2、先操作数据库再删除缓存方案

🔺线程1:操作数据库,A更新数据为B,删除缓存A

🔻线程2:查询A,缓存无数据,并把B写入缓存

这种方案下,在数据库更新成功后到删除Redis缓存数据之前的这段时间中,其他线程读取的数据都是旧数据,等Redis删除缓存后会重新从数据库中读取最新数据同步到Redis,这样可以在一定程度上保证数据的最终一致性

但是在极端情况下,线程1的缓存删除失败,线程2读取的也就是旧数据A,而不是新数据B了

这种方案也就是旁路缓存模式,那么Cache-Aside的优缺点就是:

优点

  • 简单易懂,易于实现

  • 读性能高,因为大部分读操作都会命中缓存

缺点

  • 更新数据库后缓存可能还没删除,存在短暂的不一致

  • 删除缓存后,如果数据库更新失败,会导致数据不一致

 ❓:Cache-Aside在写入请求的时候,为什么是删除缓存而不是更新缓存呢?

🔺线程1:操作数据库,更新数据为A,由于网络问题未更新缓存

🔻线程2:操作数据库,更新数据为B,更新缓存为B

🔺线程1:网络堵塞结束,更新缓存为A

那么此时缓存是A【旧数据】,数据库是B【新数据】,脏数据出现啦!!!

如果是删除缓存取代更新缓存则不会出现这个脏数据问题!!!

因此,Cache-Aside缓存模式,选择了删除缓存而不是更新缓存

适应场景:适用于读多写少的场景,特别是对数据一致性要求不是特别高的应用

2、读写穿透(Read-Through/Write-Through)

Read-Through:当缓存未命中时,自动从数据库加载数据,并写入缓存

Write-Through:当缓存更新时,同步将数据写入数据库

和旁路缓存模式很像,只有写操作不太一样

public class ReadWriteThroughPattern {private RedisService redis;private DatabaseService database;// Read-Throughpublic String readThrough(String key) {// 从缓存中获取数据String value = redis.get(key);if (value == null) {// 缓存未命中,从数据库获取数据value = database.get(key);if (value != null) {// 将数据写入缓存redis.set(key, value);}}return value;}// Write-Throughpublic void writeThrough(String key, String value) {// 将数据写入缓存redis.set(key, value);// 同步将数据写入数据库database.update(key, value);}
}

优点

  • 保证了数据的强一致性,缓存和数据库的数据始终同步。

  • 读写操作都由缓存处理,数据库压力较小。

缺点

  • 写操作的延迟较高,因为每次写入缓存时都需要同步写入数据库,增加了系统的响应时间

  • 实现复杂度较高,需要额外的缓存同步机制

适应场景:适合读多写多、且对数据一致性要求较高的场景

3、异步缓存写入(Write Behind)

异步缓存就是缓存更新后,异步批量写入数据库。这种策略适用于可以容忍一定数据不一致的高性能场景

示例代码:

public class WriteBehindPattern {private RedisService redis;private DatabaseService database;private UpdateQueue updateQueue;// 异步缓存写入public void writeBehind(String key, String value) {// 将数据写入缓存redis.set(key, value);// 异步将数据写入数据库asyncDatabaseUpdate(key, value);}private void asyncDatabaseUpdate(String key, String value) {// 异步操作,将更新请求放入队列updateQueue.add(new UpdateTask(key, value));}
}

优点

  • 写操作的性能非常高,因为只需更新缓存,数据库更新是异步进行的

  • 适用于对写操作性能要求较高的场景

缺点

  • 存在数据不一致的风险,缓存更新后数据库可能还未更新。

  • 实现复杂度较高,需要处理异步操作中的异常和重试

适应场景:大批量数据读取,允许短期数据不一致,写密集型场景

二、一致性解决方案

缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP

CAP理论,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。

没办法做到数据库与缓存绝对的一致性,但通过一些方案优化处理,是可以保证弱一致性,最终一致性

1、缓存延迟双删

流程:

  1. 先删除缓存

  2. 再更新数据库

  3. 休眠一会(比如1秒),再次删除缓存

     但休眠的时间内,可能有脏数据,且第二次删除也可能失败,导致的数据不一致问题

     延迟双删策略只能保证最终的一致性,不能保证强一致性。由于对Redis的操作和Mysql的操作不是原子性操作,所以如果想保证数据的强一致性就需要加锁控制,如下图所示

加锁之后势必会带来系统的吞吐量的下降,所以需要衡量利弊来确定是否使用加锁

方案优化:删除失败就多删除几次呀,保证删除缓存成功就可以了!

所以可以引入删除缓存重试机制

2、删除重试机制

删除缓存失败,则将这些key放入到消息队列中,消费消息队列的消息,获取要删除的key,重试删除缓存操作

3、读取biglog异步删除缓存

重试删除缓存机制还可以吧,就是会造成好多业务代码入侵

方案优化:通过数据库的binlog来异步淘汰key

        以MySQL为例,通过canal监听binlog日志感知数据的变动后,canal客户端执行删除Redis缓存数据,如果缓存数据删除失败那么发送一条MQ消息让canal客户端继续执行删除操作,这样可以保证数据的最终一致性,但是这样也增加了系统的复杂性

三、总结

(1)实际开发中一般使用使用了Cache Aside Pattern缓存更新策略模式,此方案最大程度上保证了数据的一致性并且实现也最简单

(2)无论是先操作数据库再删除缓存还是先删除缓存再操作数据库都有可能会出现删除缓存失败的情况,所以需要加入删除重试机制

(3)如果想要Redis和Mysql的数据强一致性,可以考虑使用加锁的方式实现

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 如何从网站获取表格数据
  • 第四十八天 第十章 单调栈part01 739. 每日温度 496.下一个更大元素 I 503.下一个更大元素II
  • TypeScript通过MsgPack发送数组到C++反序列化失败
  • 前端播放rtsp视频流(最后使用WebRtc)
  • MySQL环境的配置文件json
  • Redis zset 共享对象
  • OpenSNN推文:百度沈抖:深度拥抱人工智能+,加速发展新质生产力,共创智能时代新未来
  • 故障诊断 | 基于Transformer故障诊断分类预测(Matlab)
  • Godot入门 03世界构建1.0版
  • 【.NET 6 实战--孢子记账--从单体到微服务】--开发环境设置
  • 日拱一卒 | JVM
  • 哪个邮箱最安全最好用啊
  • Webpack 从入门到精通
  • PCB设计需要注意哪些事项?
  • LeetCode 2766.重新放置石块:哈希表
  • “大数据应用场景”之隔壁老王(连载四)
  • 【跃迁之路】【699天】程序员高效学习方法论探索系列(实验阶段456-2019.1.19)...
  • Create React App 使用
  • Docker: 容器互访的三种方式
  • Java 23种设计模式 之单例模式 7种实现方式
  • JS字符串转数字方法总结
  • LeetCode算法系列_0891_子序列宽度之和
  • Linux下的乱码问题
  • mysql外键的使用
  • Odoo domain写法及运用
  • SQL 难点解决:记录的引用
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • vue--为什么data属性必须是一个函数
  • 订阅Forge Viewer所有的事件
  • 官方解决所有 npm 全局安装权限问题
  • 基于Mobx的多页面小程序的全局共享状态管理实践
  • 深度学习在携程攻略社区的应用
  • 使用 Docker 部署 Spring Boot项目
  • 智能网联汽车信息安全
  • - 转 Ext2.0 form使用实例
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • #HarmonyOS:基础语法
  • (1)(1.11) SiK Radio v2(一)
  • (Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (三)Honghu Cloud云架构一定时调度平台
  • (贪心 + 双指针) LeetCode 455. 分发饼干
  • (五)activiti-modeler 编辑器初步优化
  • (原創) 如何讓IE7按第二次Ctrl + Tab時,回到原來的索引標籤? (Web) (IE) (OS) (Windows)...
  • (终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测
  • (转载)VS2010/MFC编程入门之三十四(菜单:VS2010菜单资源详解)
  • .net 7 上传文件踩坑
  • .NET MVC第三章、三种传值方式
  • .net通用权限框架B/S (三)--MODEL层(2)
  • /proc/stat文件详解(翻译)
  • [].shift.call( arguments ) 和 [].slice.call( arguments )
  • [20170705]lsnrctl status LISTENER_SCAN1
  • [BUG] Hadoop-3.3.4集群yarn管理页面子队列不显示任务
  • [BZOJ3223]文艺平衡树
  • [C++] 多线程编程-thread::yield()-sleep_for()