Redis与MySQL的数据一致性问题

news2024/9/24 1:25:24

目录

一、策略模式

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缓存模式,选择了删除缓存而不是更新缓存

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

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

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

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

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

public class ReadWriteThroughPattern {

    private RedisService redis;
    private DatabaseService database;

    // Read-Through
    public 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-Through
    public 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.coloradmin.cn/o/1946340.html

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

相关文章

【概率论】-2-概率论公理(Axioms of Probability)

上一篇文章我们学习了基本的概率论内容-排列组合,本次我们学习概率论公理的内容,正式开始计算概率,在开始前我们需要学习一些基本概念。 目录 一.样本空间和事件 1.样本空间 2.事件 3.交并补 二、概率公理 1.基本公理 2.对称差 2.布尔…

Mysql中(基于GTID方式)实现主从复制,单主复制详细教程

🏡作者主页:点击! 🐧Linux基础知识(初学):点击! 🐧Linux高级管理防护和群集专栏:点击! 🔐Linux中firewalld防火墙:点击! ⏰️创作…

Android Studio 一键删除 Recent Projects信息的方法

Android Studio打开项目多了就一堆最近项目的记录,在IDE里面只能一个个手动删除。 File - Recent Projects 解决方案:修改配置文件 Note:方法不唯一。 Android Studio 存储了一个包含最近打开项目信息的配置文件。通过手动编辑或删除recentP…

代码随想录算法训练营day7 | 454.四数相加II、383.赎金信、15.三数之和、18.四数之和

文章目录 454.四数相加II思路 383.赎金信思路 15.三数之和思路剪枝去重 18.四数之和思路剪枝去重复习:C中的类型转换方法 总结 今天是哈希表专题的第二天 废话不多说,直接上题目 454.四数相加II 建议:本题是 使用map 巧妙解决的问题&#x…

Pytorch使用教学1-Tensor的创建

0 导读 在我们不知道什么是深度学习计算框架时,我们可以把PyTorch看做是Python的第三方库,在PyTorch中定义了适用于深度学习的张量Tensor,以及张量的各类计算。就相当于NumPy中定义的Array和对应的科学计算方法,正是这些基本数据…

JVM系列(三) -类加载器及双亲委派模型介绍

在之前的文章中,介绍了类的加载过程中,我们有提到在加载阶段,通过一个类的全限定名来获取此类的二进制字节流操作,其实类加载器就是用来实现这个操作的。 在虚拟机中,任何一个类,都需要由加载它的类加载器…

【ffmpeg命令入门】添加水印

文章目录 前言什么是水印?为什么要添加水印?ffmpeg添加水印添加图片水印添加文字水印基本使用方法drawtext的参数 总结 前言 在视频制作和编辑的过程中,添加水印是一个常见且重要的步骤。水印不仅可以保护版权,还能用于品牌宣传和…

netty入门-4 Channel与ChannelFuture

Channel 基本类似于NIO中的Channel概念。作为读写数据的通道。 常见方法 close() 可以用来关闭 channelcloseFuture() 用来处理 channel 的关闭 sync 方法作用是同步等待 channel 关闭而 addListener 方法是异步等待 channel 关闭 pipeline() 方法添加处理器write() 方法将数…

Stable Diffusion基本原理通俗讲解

Stable Diffusion是一种基于深度学习的图像生成技术,它属于生成对抗网络(GANs)的一种。简单来说,Stable Diffusion通过训练一个生成器(Generator)和一个判别器(Discriminator)&#…

算法力扣刷题记录 五十八【701.二叉搜索树中的插入操作】

前言 本文是二叉搜索树操作。 二叉树篇继续。 一、题目阅读 给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节…

【常见开源库的二次开发】基于openssl的加密与解密——SHA算法源码解析(六)

目录 一、SHA-1算法分析: 1.1 Merkle Tree可信树 1.2 源码实现: 1.3 哈希计算功能 1.4 两种算法的区别: 1.4.1 目的 1.4.2 实现机制 1.4.3 输出 1.4.4 应用场景: 1.4 运行演示: 二、SHA-2算法分析: 2.1哈…

【ESP32S3学习笔记】与有人AP520X路由器连接失败的问题

项目场景: 提示:这里简述项目相关背景: 项目上新换了个路由器,结果发现ESP32模组连接不上,其他的路由器都正常。 问题描述 提示:这里描述项目中遇到的问题: 对比log发现有问题的时候&#x…

智慧大棚数据库版

创建一个SMartBigHouse数据库 在数据库创建一个表用来存储数据 这边将id设为主键并将标识增量设为1 搭建Winfrom 搭建历史查询界面 串口数据,(这边是用的一个虚拟的串口工具,需要的话私) ModbusSerialMaster master;DataPointCollection wenduValues; //…

Win10使用VS Code远程连接Ubuntu服务器时遇到SSH公钥错误的解决方案

在使用Windows 10上的Visual Studio Code(VS Code)远程连接Ubuntu 20.04服务器时,遇到了以下错误: 错误的原因 这个错误消息表明,SSH 客户端检测到远程主机的 ECDSA 公钥已更改。可能是由于以下原因之一&#xff1a…

python—NumPy的基础(2)

文章目录 一维数组索引和切片一维数组索引和切片的使用一维数组负索引和切片的使用 二维数组的索引和切片索引直接获取使用坐标获取数组[x,y]二维数组负索引的使用切片数组的复制 改变数组的维度改变数组的维度 数组的拼接列表的拼接一维数组的拼接二维数组的拼接vstack 与hsta…

el-image预览图片点击遮盖处关闭预览

预览关闭按钮不明显 解决方式: 1.修改按钮样式明显点: //el-image 添加自定义类名,下文【test-image】代指 .test-image .el-icon-circle-close{ color:#fff; font-size:20px; ...改成很明显的样式 }2.使用事件监听,监听当前遮…

第十一章 数据结构

第十一章 数据结构 11.1 数组 数组是元素的顺序集合,通常这些元素具有相同的数据类型 索引表示元素在数组中的顺序号,顺序号从数组开始处计数 数组元素通过索引被独立给出了地址,数组整体上有一个名称,但每个元素利用数组的的…

TCP网络socket编程(面向连接)

Tcp面向链接、面向字节流和文件的读写非常类似():客户端创建套接字主动建立连接,服务器监听套接字一直等待连接的到来,监听到一个,就创建一个新的套接字用于IO 服务器: 创建套接字&#xff1a…

区块链和数据要素融合的价值及应用

一、数据要素面临的关键障碍 在构建数据要素基石的过程中,首要任务是明确并解决产权架构的难题,特别是使用权的确立与流转机制的顺畅,此乃数字经济蓬勃发展的命脉所在。一个高效的数据流转体系对于激发数据潜能、加速经济发展及优化数据资源…

TreeSelect增加可筛选功能

TreeSelect官方可筛选示例 <template><el-tree-selectv-model"value":data"data"filterablestyle"width: 240px"/><el-divider /><el-divider />filter node method:<el-tree-selectv-model"value":data&q…