关于缓存数据一致性的解决方案

news2024/12/25 9:09:33

缓存数据一致性

引入缓存会导致一些比如修改/删除内容后缓存还是之前的数据,这会导致缓存和数据库数据不一致的情况,本文将提到相关的解决方案,而且还提供了canal去实现每次在更新数据库的时候自动同步缓存,而无需将代码都写在后端造成冗余。

缓存的更新方案(传统)

先更新DB还是先更新缓存?是更新缓存还是删除缓存?在常规情况下,怎么操作都可以,但一旦存在高并发场景,就需要采用合适的方案。

1、先更新数据库再更新缓存(双写策略1)

线程A:更新数据库(第1s)——> 更新缓存(第10s)

线程B:更新数据库 (第3s)——> 更新缓存(第5s)

并发场景下,这样的情况是很容易出现的,每个线程的操作先后顺序不同,这样就导致请求B的缓存值被请求A给覆盖了,数据库中是线程B的新值,缓存中是线程A的旧值,并且会一直这么脏下去直到缓存失效(设置了过期时间)

2、先更新缓存再更新数据库(双写策略2)

线程A:更新缓存(第1s)——> 更新数据库(第10s)

线程B: 更新缓存(第3s)——> 更新数据库(第5s)

和前面一种情况相反,缓存中是线程B的新值,而数据库中是线程A的旧值。

前两种方式之所以会在并发场景下出现异常,本质上是因为更新缓存和更新数据库是两个操作,我们没有办法控制并发场景下两个操作之间先后顺序,也就是先开始操作的线程先完成自己的工作。

3、先删除缓存再更新数据库

通过这种方式,我们很惊喜地发现,前面困扰我们的并发场景的问题确实被解决了!两个线程都只修改数据库,不管谁先,数据库以之后修改的线程为准。

但这个时候,我们来思考另一个场景:两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的。很显然,这种状况也不是我们想要的。

延时双删方案(大多数)

传统方案无论怎么样似乎都会出现漏洞导致缓存数据不一致的问题。大部分企业都是采用的延时双删方案,这种方案很简单而且高效。

1.删除缓存

2.更新数据库

3.睡眠一段时间(500ms)

4.再次删除缓存

加了个睡眠时间,主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。

所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。

但是具体睡眠多久其实是个玄学,很难评估出来,所以这个方案也只是尽可能保证一致性而已,极端情况下,依然也会出现缓存不一致的现象。因此,还是不太建议这种方案。

在这里插入图片描述

对于蓝色的文字,“删除缓存 10”必须在“回写缓存10”后面,那如何才能保证一定是在后面呢?让请求 A 的最后一次删除,等待 500ms。

弊端: 在读写模式(MySQL主从复制)同时存在的情况下,会有数据一致性问题

在这里插入图片描述

分布式读写锁

  • 读读允许并发
  • 读写不允许并发
  • 写读不允许并发
  • 写写不允许并发

依赖“锁”的机制,避免出现并发读写。弊端:性能低

读数据方法(查询操作)

@Autowired
private RedissonClient redissonClient;


/**
 * 读取数据方法 允许并发读。 但不允许进行并发读写,写读 
 *
 * @return
 */
@Override
public String read() {
    System.out.println("read当前节点被调用:" + port);
    //1.创建读写锁对象
    RReadWriteLock rwlock = redissonClient.getReadWriteLock("myLock");
    //2.获取读锁对象
    RLock lock = rwlock.readLock();
    //3.获取读锁
    lock.lock(5, TimeUnit.SECONDS);

    //优先从缓存中获取数据
    String data = redisTemplate.opsForValue().get("data");
    if (StringUtils.isNotBlank(data)) {
        //TODO 模拟查询数据库
        data = "dbData";
        //将查询结果放入缓存
        redisTemplate.opsForValue().set("daa", data);
    }

    //todo 故意不释放读锁  - 5s后自动释放
    lock.unlock();
    return data;
}

修改数据方法(修改操作)

/**
 * 写数据方法  不允许并发读写,写读。 先哪个操作来的先执行谁,等待写锁或者读锁释放,写数据方法才能继续执行
 */
@Override
public void write() {
    System.out.println("write当前节点被调用:" + port);
    //1.创建读写锁对象
    RReadWriteLock rwlock = redissonClient.getReadWriteLock("myLock");
    //2.获取写锁对象
    RLock lock = rwlock.writeLock();
    //3.获取写锁
    lock.lock(5, TimeUnit.SECONDS);

    //删除缓存
    redisTemplate.delete("data");

    //模拟修改数据库
    String data = "writeData";

    //todo 故意释放写锁  -- 5s后自动释放
    lock.unlock();
}

监听 MySQL binlog方案(推荐)

先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。

于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。

alibaba/canal: 阿里巴巴 MySQL binlog 增量订阅&消费组件 (github.com)

Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。

下图是 Canal 的工作原理:

在这里插入图片描述

所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。

验证MySQL是否开启BinLog确保Value为 ON

show variables like '%log_bin%';

在这里插入图片描述

MySQL新增用户用于监听Binlog日志,在Canal容器中要使用该用户

#创建用户
CREATE USER canal IDENTIFIED BY '123456';  
#给用户授权
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
#如果是MySQL8.X以上需要对加密方式进行设置
ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
#刷新生效
FLUSH PRIVILEGES;

采用Docker方式创建Canal服务端。以下为创建Canal容器命令,要修改要监听主MySQL数据库IP跟端口用户名以及密码确保正确,别忘了把ip改成自己的。这里用的是1.1.5版本的canal

docker run -p 11111:11111 --name canal \
-e canal.destinations=tingshuTopic \
-e canal.instance.master.address=192.168.200.6:3306  \
-e canal.instance.dbUsername=canal  \
-e canal.instance.dbPassword=123456  \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-e canal.instance.filter.regex=.*\\..* \
-d canal/canal-server:v1.1.5

在java项目中引入依赖

<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>persistence-api</artifactId>
    <version>1.0</version>
</dependency>

在application.yml中配置canal的相关信息

#canal配置
canal:
  destination: tingshuTopic #Canal服务端发送数据的话题名称跟上面容器里参数destinations的一样
  server: 192.168.200.6:11111

提供Java实体类类监听变更后的数据,注意属性上使用**@Column**注解进行映射(这只是示例)。这里不需要提供表中全部的字段,只要与修改相关的即可。

//监听变更表
@Data
public class UserCDC {

    @Column(name = "id")
    private Long id;


    @Schema(description = "nickname")
    @TableField("nickname")
    @Column(name = "nickname")
    private String nickname;

    @Schema(description = "头像图片")
    @TableField("avatar_url")
    @Column(name = "avatar_url")
    private String avatarUrl;


    @Schema(description = "性别")
    @TableField("gender")
    @Column(name = "gender")
    private Integer gender;

    @Schema(description = "出生年月")
    @TableField("birthday")
    @Column(name = "birthday")
    private Date birthday;
    
}

监听到变更业务处理类(这只是测试)

/**
 *
 */
@CanalTable("user") //监控指定表变更操作
@Component
public class SkuInfoHandler implements EntryHandler<SkuInfoCDC> {

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 监听到新增操作
     *
     * @param skuInfo
     */
    @Override
    public void insert(UserCDC user) {
        System.out.println("新增用户");
        System.out.println("user = " + user);
    }

    /**
     * 监听到修改操作
     *
     * @param before
     * @param after
     */
    @Override
    public void update(UserCDC before, UserCDC after) {
        System.out.println("修改用户");
        System.out.println("修改用户before:" + before);
        System.out.println("修改用户after:" + after);
        try {
            //删除缓存
            String dataKey = RedisConst.SKUKEY_PREFIX + after.getId() + RedisConst.SKUKEY_SUFFIX;
            redisTemplate.delete(dataKey);
        } catch (Exception e) {
            throw new RuntimeException(e);
            //todo 将删除缓存失败消息发送MQ-将来MQ消费者监听后进行重试
        }
    }

    /**
     * 监听到删除操作
     *
     * @param skuInfo
     */
    @Override
    public void delete(UserCDC usercdc) {
    }
}

注意:canal版本可能还不支持springboot3(至少在现在的1.1.5版本是不支持的),所以无法在本项目中使用canal。只能新建一个专门的项目去使用。还有就是如果CDC类里面有字段是null会造成封装失败。

ps:如果在springboot3环境下去用这个,请注意一定要将设置里java编译器给cdc模块设置专门的jdk版本(并且不要填默认值!),项目结构也要设置专门的8版本。

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

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

相关文章

操作系统基础:进程同步【上】

&#x1f308;个人主页&#xff1a;godspeed_lucip &#x1f525; 系列专栏&#xff1a;OS从基础到进阶 1 进程同步&#xff08;上&#xff09;1.1 进程同步与互斥1.1.1 进程同步1.1.1.1 必要性1.1.1.2 什么是进程同步 1.1.2 进程互斥1.1.2.1 必要性1.1.2.2 什么是进程互斥1.1.…

RTC 滴答计时器

1. RTC 滴答计时器 1.1 寄存器配置 RTCCON RTC控制寄存器 [7:4] 0000 设置频率 [8] 1 使能滴答计时器 TICNT 32位滴答时间计数值。 滴答计时器是一个上行计数器。如果当前的滴答数达到这个值&#xff0c;滴答时间中断发生。 备注:该值必须大于3 周期 (n 1)/滴答时钟…

文件制作二维码的图文教学,多种格式都可以使用

现在我们经常会发现在扫描二维码的时候&#xff0c;可能一个二维码中会存在多个文件或者多个二维码中会显示不同的文件的&#xff0c;那么这些文件存入二维码中是用什么方法制作的呢&#xff1f; 文件二维码的制作方法其实很简单&#xff0c;只需要通过文件二维码生成器工具的…

这都2024年了 你还要多久才能领悟 LinkedList 源码

这都2024年了 你还要多久才能领悟 LinkedList 源码 文章目录 这都2024年了 你还要多久才能领悟 LinkedList 源码LinkedList 简介LinkedList 插入和删除元素的时间复杂度&#xff1f;LinkedList 为什么不能实现 RandomAccess 接口&#xff1f; LinkedList 源码分析初始化插入元素…

【数据结构之二叉树的构建和遍历】

数据结构学习笔记---009 数据结构之二叉树1、二叉树的概念和结构1.1、回顾二叉树的重要性质1.2、回顾二叉树的主要分类1.1、如何实现二叉树&#xff1f; 2、二叉树的实现2.1、二叉树的BinaryTree.h2.2、二叉树的BinaryTree.c2.2.1、二叉树的构建2.2.2、二叉树销毁2.2.3、二叉树…

RabbitMQ入门概念

目录 一、RabbitMQ入门 1.1 rabbitmq是啥&#xff1f; 1.2 应用场景 1.3 AMQP协议与RabbitMQ工作流程 1.4 Docker安装部署RabbitMQ 二、SpringBoot连接MQ配置 2.1 示例1 2.1 示例2 —— 发送实体 一、RabbitMQ入门 1.1 rabbitmq是啥&#xff1f; MQ&#xff08;Message…

Hutool导入导出用法

整理了下Hutool导入导出的简单使用。 导入maven或jar包&#xff08;注意这里导入的poi只是为了优化样式&#xff09; <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all --> <dependency><groupId>cn.hutool</groupId><artifactId&g…

Kube-Promethus配置Nacos监控

Kube-Promethus配置Nacos监控 前置&#xff1a;Kube-Promethus安装监控k8s集群 一.判断Nacos开启监控配置 首先通过集群内部任一节点访问Nacos的这个地址<NacosIP>:端口号/nacos/actuator/prometheus&#xff0c;查看是否能够获取监控数据。 如果没有数据则修改Nacos集群…

qt中使用mysql 数据库

QT 版本介绍 虽然版本是这个&#xff0c;但是工作目录确是&#xff1a; 下面陈述安装步骤 第一步&#xff1a; 就是安装MYSQL 数据库&#xff0c;在此不再赘述了&#xff0c;很多博主已经上传了。 第二步&#xff1a; 就是拷贝QT 对应mysql 的版本驱动到 QT 的编译器文件中…

基于人工智能的质量保证(QA)流程

AI模型质量保证需知 推出准确、可靠、公正的人工智能&#xff08;AI&#xff09;模型无疑是一项挑战。设法成功实施AI计划的企业很可能意识到&#xff0c;AI质量保证&#xff08;QA&#xff09;流程与传统QA流程迥然不同。 质量保证对于AI模型的准确性至关重要&#xff0c;不…

SD卡写保护无法格式化怎么办?

一般来说&#xff0c;写保护&#xff08;也称为只读&#xff09;是数据存储设备防止写入新数据或修改旧信息的能力。换句话说&#xff0c;您可以读取存储在磁盘上的信息&#xff0c;但是却不能删除、更改或复制它们&#xff0c;因为访问会被拒绝。那么SD卡有写保护怎么格式化呢…

【图像拼接 精读】Parallax-Tolerant Unsupervised Deep Image Stitching

【精读】Parallax-Tolerant Unsupervised Deep Image Stitching 在这篇论文中&#xff0c;"warp"&#xff08;变形&#xff09;和"composition"&#xff08;组合&#xff09;是两个关键的概念。"Warp"指的是图像变形的过程&#xff0c;用于调整…

【乳腺肿瘤诊断分类及预测】基于LVQNN学习向量量化神经网络

课题名称&#xff1a;基于LVQ神经网络的乳腺肿瘤诊断&#xff08;类型分类&#xff09; 版本日期&#xff1a;2023-03-10 运行方式: 直接运行0501_LVQ0501.m 文件即可 代码获取方式&#xff1a;私信博主或QQ&#xff1a;491052175 模型描述&#xff1a; 威斯康辛大学医学院…

NetCore实现输入用户名和密码后访问Swagger页面

1 原理说明 在后端编程时&#xff0c;通常使用swagger文档来呈现接口文档。为了接口的安全性&#xff0c;可通过输入用户名和密码的方式来进行验证。 这里用到了Basic认证方式。原理图如下&#xff1a; 步骤 1&#xff1a; 当请求的资源需要 B A S I C \textcolor{red}{BA…

力扣238. 除自身以外数组的乘积(前后缀和)

Problem: 238. 除自身以外数组的乘积 文章目录 题目描述思路复杂度Code 题目描述 思路 思路1&#xff1a; 1.先求取数组的包括当前下标值得前后缀乘积&#xff08;利用两个数组记录下来分别为leftProduct和rightProduct&#xff09; 2.当求取一个下标为i的数组中的元素&#x…

正则表达式补充以及sed

正则表达式&#xff1a; 下划线算 在单词里面 解释一下过程&#xff1a; 在第二行hello world当中&#xff0c;hello中的h 与后面第一个h相匹配&#xff0c;所以hello中的ello可以和abcde匹配 在world中&#xff0c;w先匹配h匹配不上&#xff0c;则在看0&#xff0c;r&#…

【Java 数据结构】LinkedList与链表

LinkedList与链表 1. ArrayList的缺陷2. 链表2.1 链表的概念及结构2.2 链表的实现 3. LinkedList的模拟实现4.LinkedList的使用4.1 什么是LinkedList4.2LinkedList的使用 5. ArrayList和LinkedList的区别 1. ArrayList的缺陷 上节课已经熟悉了ArrayList的使用&#xff0c;并且…

多头 eRCD(Multi-Headed eRCD)

&#x1f525;点击查看精选 CXL 系列文章&#x1f525; &#x1f525;点击进入【芯片设计验证】社区&#xff0c;查看更多精彩内容&#x1f525; &#x1f4e2; 声明&#xff1a; &#x1f96d; 作者主页&#xff1a;【MangoPapa的CSDN主页】。⚠️ 本文首发于CSDN&#xff0c…

在哪里申请SSL证书

其实只是单纯的申请SSL证书来说&#xff0c;渠道还是比较多的。只是需要格外注意在申请SSL证书的过程中&#xff0c;对于自身需求的认知。 首先最重要的是&#xff0c;该证书是否可信。就目前而言&#xff0c;非可信根的证书是无法与主流浏览器兼容的&#xff0c;会时常发生风险…

503 Service Temporarily Unavailable nginx 原因和解决办法

前言 HTTP 503 Service Temporarily Unavailable 错误通常表示服务器无法处理请求&#xff0c;可能是由于服务器过载、维护或其他临时性问题导致的。在 Nginx 中&#xff0c;这种错误通常与后端服务的可用性问题相关。以下是可能的原因和解决办法&#xff1a; 正文…