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的数据强一致性,可以考虑使用加锁的方式实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/51212.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python】sklearn基础教程及示例

【python】sklearn基础教程及示例 Scikit-learn(简称sklearn)是一个非常流行的Python机器学习库,提供了许多常用的机器学习算法和工具。以下是一个基础教程的概述: 1. 安装scikit-learn 首先,确保你已经安装了Python和…

Unity 资源 之 Pop It 3D 解压玩具与双人AI游戏 Unity 资源包分享

精彩呈现:Pop It 3D 解压玩具与双人AI游戏 Unity 资源包分享 一、Pop It 3D 解压玩具的魅力二、双人游戏的互动乐趣三、Unity 游戏资源包的优势四、如何获取资源包 亲爱的游戏爱好者们,今天为大家带来一款令人兴奋的游戏资源——Pop It 3D 解压玩具双人带…

数字陷波器的设计和仿真(Matlab+C)

目录 一、数字陷波器的模型 二、Matlab仿真 1. 示例1 2. 示例2 三、C语言仿真 1. 由系统函数计算差分方程 2. 示例代码 一、数字陷波器的模型 二、Matlab仿真 1. 示例1 clear clc f0=100;%滤掉的100Hz fs=1000;%大于两倍的信号最高频率 r=0.9; w0=2*pi*f0/fs;%转换到…

[图解]《分析模式》漫谈19-Midjourney、Sora

1 00:00:02,360 --> 00:00:03,360 今天的漫谈 2 00:00:03,370 --> 00:00:04,560 我们来说一下 3 00:00:04,570 --> 00:00:08,720 人工智能,还是前言 4 00:00:08,890 --> 00:00:11,840 这里有一句话 Kent Beck 5 00:00:12,630 --> 00:00:13,750 W…

Spring Boot配置文件的语法规则

主要介绍两种配置文件的语法和格式,properties和yml 目录 1.配置文件的作用 2.创建配置文件 3.properties语法 4.yml语法 5.配置文件格式 1.配置文件的作用 对于配置文件,也有独立的文件夹去存放,主要用来存放一些需要经过变动的数据&a…

python绘制方波信号

python绘制方波信号 1、效果 2、导入库 pip install numpy pip install matplotlib3、实现代码 # -*- coding: utf-8 -*-""" @contact: 微信 1257309054 @file: test.py @time: 2024/7/28 14:48 @author: LDC """ import numpy as np import …

IOS-04 Swift 中数组、集合、字典、区间、元组和可选类型

在 Swift 编程语言中,数据结构和类型的合理运用对于高效编程至关重要。接下来,我们将深入探讨数组、集合、字典、区间、元组和可选类型的相关知识。 一、数组(Array) (一)元素定义 可以通过多种方式定义数…

Hello 算法:动画图解、一键运行的数据结构与算法教程

Hello 算法 《Hello 算法》是一份开源、免费的数据结构与算法入门教程,特别适合新手。全书采用动画图解,内容清晰易懂,学习曲线平滑,引导初学者探索数据结构与算法的知识地图。源代码可以一键运行,帮助读者通过练习提…

C#中的同步编程和异步编程

1. 简单描述一下同步编程和异步编程 同步编程:按照代码的顺序一行一行执行,如果某个操作需要等待(比如读取文件、网络请求、数据库操作等),那么当前的线程就会停下来,一直到这个操作完成了之后&#xff0c…

git学习(一)

一、代码仓库的初始化 1、先在本地操作,不涉及到远程服务器,创建目录mkdir git demo 想要本地创建的目录成为一个远程仓库就需要初始化git init git init 后会发生什么? 2、watch -n 1 -d tind每隔1s打印当前文件目录并且刷新 左边命令 wa…

搞DDR,你是可以看看我的这篇笔记(三)

关于DDR PHY这个部分,是数模混合器件,工作涉及到了很多信号完整性,眼图,模拟等相关的东西我就没讲了。因为确实不太熟悉,只能站在架构、功能、使用上去聊聊。 上一篇我们看了这个图片,简化就是下面这个样子: 其实这个也不太合适~~~ 这样舒服多了,一般DDRC和DDRPHTY都会…

被工信部认可的开源软件治理解决方案

近日,工信部网络安全产业发展中心正式发布了“2023年信息技术应用创新解决方案”,开源网安凭借“基于SCA技术开源软件治理解决方案”顺利入选,成为经工信部认可的优秀解决方案,这是开源网安连续两届荣获此荣誉。 工业和信息化部网…

17.延迟队列

介绍 延迟队列,队列内部是有序的,延迟队列中的元素是希望在指定时间到了以后或之前取出和处理。 死信队列中,消息TTL过期的情况其实就是延迟队列。 使用场景 1.订单在十分钟内未支付则自动取消。 2.新创建的店铺,如果十天内没…

【Redis系列】RedisTemplate的使用与注意事项

目录 一.什么是RedisTemplate 二.如何使用RedisTemplate RedisTemplate的API 序列化 三.StringRedisTemplate 一.什么是RedisTemplate RedisTemplate 是一个工具类,由 Spring 官方提供的方便操作 Redis 数据库的一个工具类,来源于 org.springframe…

【LLM】-10-部署llama-3-chinese-8b-instruct-v3 大模型

目录 1、模型下载 2、下载项目代码 3、启动模型 4、模型调用 4.1、completion接口 4.2、聊天(chat completion) 4.3、多轮对话 4.4、文本嵌入向量 5、Java代码实现调用 由于在【LLM】-09-搭建问答系统-对输入Prompt检查-CSDN博客 关于提示词注入…

涉密移动载体智能柜管控系统DW-S404|国产自主可控

东识移动载体管控系统载体柜系统采用RFID识别技术,结合智能载体管理软件,实现了文件载体权限管理、定位管理、智能存取、智能盘点、在线监控等功能,同时对文件载体进行规范化、智能化、自动化管理。工作人员通过授权进行文件、载体、卷宗等存…

C# 简单的单元测试

文章目录 前言参考文档新建控制台项目新建测试项目添加引用添加测试方法测试结果(有错误)测试结果,通过正规的方法抛出异常 总结 前言 听说复杂的项目最好都要单元测试一下。我这里也试试单元测试这个功能。到时候调试起来也方便。 参考文档 C# 单元测试&#xf…

这一文,关于 Java 泛型的点点滴滴 一(泛型基础、类型擦除)

作为一个 Java 程序员&#xff0c;用到泛型最多的&#xff0c;我估计应该就是这一行代码&#xff1a; List<String> list new ArrayList<>();这也是所有 Java 程序员的泛型之路开始的地方啊。 不过本文讲泛型&#xff0c;先不从这里开始讲&#xff0c;而是再往前…

富唯智能转运机器人:高效、智能、未来的选择

在现代工业中&#xff0c;高效的物流和物料处理是提升生产效率的关键。富唯智能转运机器人&#xff0c;以其卓越的技术和智能化的设计&#xff0c;为各行业提供了完美的解决方案。 产品概述 富唯智能转运机器人搭载ICD系列核心控制器&#xff0c;拥有多种移载平台&#xff0c…

dpdk编译安装以及接收udp报文(基于ubuntu)

目录 1、编译 2、设置运行环境 3、使用dpdk接收udp报文 3.1、设置发送端arp信息 3.2、测试 3.3、代码 4、其他 1、编译 代码下载&#xff1a; DPDK 下载版本&#xff1a;DPDK 19.08.2 export RTE_SDK/root/dpdk-stable-19.08.2/ export RTE_TARGETx86_64-native-li…