redis进阶:mysql,redis双写一致性,数据库更新后再删除缓存就够了吗?

news2024/11/25 7:38:11

0. 引言

最近线上的一个状态修改功能出现了问题,一开始是运营找了过来,运营告知某条数据的状态已经开启了的,但是实际使用起来还是没有生效,于是拿到这个问题后,首先就去数据库查了这条数据,发现确实如他所说,状态数据是已经更改过的。但是为什么没有生效呢?

于是再次查看了获取数据的方法,发现是优先获取的缓存,于是查询缓存里的数据,发现了问题所在,缓存里并没有将这条数据的状态更改过来,也就是缓存数据不一致问题。

继续追查状态修改的方法,发现采用的更新方式是先更新数据库,然后删除缓存。这是我们常用的双写一致性的处理方法,但也正是这样的方式出现了问题。下面我们来详细讲解

1. 先更新数据库,再删除缓存为什么有问题?

首先原来的方法,大概是这样的,将数据库更新后,再将缓存删除掉

    public R save(UserVO userVO) {
        User user = new User();
        BeanUtils.copyProperties(userVO, user);
        saveUser(user);
        redisTemplate.delete("userInfo:" + user.getUserName());
        return R.success("操作成功");
    }

同时在获取数据时,采用的是先从缓存获取,如果缓存没有,就从数据库查询,并同时存放到缓存上,这样保证了下次访问时数据能直接从缓存获取,减少了数据库压力

    public User getByUserName(String userName) {
        User user = (User) redisTemplate.opsForValue().get(userName);
        if (user != null) {
            return user;
        }
        user = this.getOne(Wrappers.<User>lambdaQuery().eq(User::getUserName, userName));
        redisTemplate.opsForValue().set("userInfo:" + userName, user);
        return user;
    }

数据库的数据已经更新成功了,说明数据库是没有报错了,那么哪个地方报错了呢?当然是redis,在执行redis的删除操作时,如果发现错误,比如因为网络问题,或者redis本身服务问题,导致没有正常执行而产生的报错,所以为了捕获这个报错,我们需要添加上数据库事务 @Transactional(rollbackFor = Exception.class)

    @Transactional(rollbackFor = Exception.class)
    public R save(UserVO userVO) {
        User user = new User();
        BeanUtils.copyProperties(userVO, user);
        saveUser(user);
        redisTemplate.delete("userInfo:" + user.getUserName());
        return R.success("操作成功");
    }

但是这样你以为就结束了吗,这样的操作确实可以实现事务的回滚,但是上述的模式仅仅只是我们用到的Cache Aside模式(这里大家要了解redis缓存的四种模式),而对于另外一种同样经典的Write Through模式就不再适用于这个操作了。

2. Cache Aside模式与Write Through模式的区别

Cache Aside模式采用的是读时放缓存或者预加载缓存,写时更新数据库,删缓存,它适用于读多写少,因为缓存没有时要去数据库查数据,如果多线程场景下,同一时间打进来同样的请求,都会去访问数据库,一方面容易造成缓存击穿,一方面并不能保证数据的强一致性,比如如下场景:

两个请求同时执行:
请求A查询用户A信息,请求B更新用户A信息
请求A查询缓存没有值,于是去查询数据库,获取到值为old(还没有更新缓存)
请求B更新数据库用户A信息为new
请求B删除掉用户A信息的缓存
请求A写入缓存,值为old
至此,就导致用户A的数据缓存为旧数据了

Write Through模式采用的是读缓存,写时先更新缓存,再更新数据库,一般缓存不设置过期时间,适用于频繁查询缓存数据的场景。因为是先更新缓存,再更新数据库,且在查询操作时不做更新缓存,所以保证了数据的一致性,也防止了缓存击穿。

3. Write Through模式下的双写一致性

因为是先更新缓存,再更新数据库,那么更新方法的代码示例就变成了如下所示:

    @Transactional(rollbackFor = Exception.class)
    public R save3(UserVO userVO) {
        User user = new User();
        // 拷贝属性
        BeanUtils.copyProperties(userVO, user);
        redisTemplate.opsForValue().set(user.getUserName(), user);
        userMapper.addUser(user);
        return R.success("操作成功");
    }

但是这样的操作,肯定是有问题的,因为一旦数据库报错,@Transactional能够保证数据库回滚,但并不能保证redis的事务性,于是我们需要让redis也能保证事务

方案一: SessionCallback实现redis事务

以为redis本身就自带事务指令,所以最容易想到的就是通过事务指令来实现redis事务,结合redisTemplate实现,需要借助SessionCallback回调类,实现代码如下

   @Transactional(rollbackFor = Exception.class)
    public R save2(UserVO userVO) {
        // 本地事务+redis事务 = 双写一致性/缓存强一致性/redis.mysql事务回滚
        User user = new User();
        // 拷贝属性
        BeanUtils.copyProperties(userVO, user);

        Object execute = redisTemplate.execute(new SessionCallback<List<Boolean>>() {
            @Override
            public List<Boolean> execute(RedisOperations operations) throws DataAccessException {
                // 事务开启
                operations.multi();
                // 执行的操作,redis先操作
                operations.opsForValue().set(user.getUserName(), user);
                try {
                    saveUser(user);
                } catch (Exception e) {
                    // 事务取消
                    operations.discard();
                    e.printStackTrace();
                    return null;
                }
                // 事务的提交
                return operations.exec();
            }
        });
        return R.status(execute != null);
    }

方案二: setEnableTransactionSupport实现redis事务

这样的代码还是比较长的,书写起来略显麻烦,能不能有更简单的方法,或者说能不能让redis也适配 @Transactional注解,实现事务操作。答案是当然可以的,在redisTemplate中有这样的一个方法setEnableTransactionSupport(true),它可以开启redis支持数据库事务

@Configuration
public class RedisConfig {

    /**
     * 创建RedisTemplate
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        // 创建对象
        RedisTemplate redisTemplate = new RedisTemplate();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 设置redis生成的key的序列化器,对key编码进行处理
        RedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        // 设置redis支持数据库事务
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }
}

开启之后,我们的更新代码就变成了如下所示,非常的简洁清爽

    @Transactional(rollbackFor = Exception.class)
    public R save3(UserVO userVO) {
        User user = new User();
        // 拷贝属性
        BeanUtils.copyProperties(userVO, user);
        redisTemplate.opsForValue().set(user.getUserName(), user);
        userMapper.addUser(user);
        return R.success("操作成功");
    }

测试

最后我们来进行一个模拟测试,我们将sql中的字段故意写成不存在的字段名

在这里插入图片描述

请求这个接口

在这里插入图片描述

报错字段password2不存在

在这里插入图片描述
查看数据库中并没有添加成功

在这里插入图片描述

缓存里同样也没有,说明我们的redis支持了数据库的事物操作

在这里插入图片描述

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

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

相关文章

深入了解字典树

字典树&#xff08;Trie&#xff09; 目录字典树&#xff08;Trie&#xff09;一、问题引入二、字典树介绍3、字典树的实现4、存储与查询一、问题引入 现有长度为n的字符串数组&#xff0c;[“go”&#xff0c;“goog”&#xff0c;“google”&#xff0c;“golang”&#xff0…

【数据结构入门】-链表之双向循环链表

个人主页&#xff1a;平行线也会相交 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【数据结构初阶&#xff08;C实现&#xff09;】 文章目录链表初始化打印链表尾插尾删新建一个节点头插头删查找在pos之前插入*删除pos位…

CSS中的伪元素和伪类

一直被伪类和伪元素所迷惑&#xff0c;以为是同一个属性名称&#xff0c;根据CSS动画&#xff0c;索性开始研究a:hover:after&#xff0c;a.hover:after的用法。 伪元素 是HTML中并不存在的元素&#xff0c;用于将特殊的效果添加到某些选择器。 对伪元素的描述 伪元素有两…

【Verilog】握手信号实现跨时钟域数据传输-handshake

文章目录handshake握手电路使用握手信号实现跨时钟域数据传输接口信号图题目描述解题思路代码设计数据发送模块data_driver数据接收模块data_receivertestbench波形handshake握手电路 跨时钟域处理是个很庞大并且在设计中很常出现的问题握手(handshake)是用来处理信号跨时钟域…

数字化引领乡村振兴,VR全景助力数字乡村建设

一、数字乡村建设加速经济发展随着数字化建设的推进&#xff0c;数字化农业产业正在成为农业产业发展的主导力量&#xff0c;因此数字化技术赋予农业产业竞争力的能力不可小觑。数字化乡村建设背景下&#xff0c;数字化信息技术将全面改造升级农村产业&#xff0c;从农业、养殖…

new set数组对象去重失败

我们知道Set是JS的一个种新的数据结构&#xff0c;和数组类似&#xff0c;和数组不同的是它可以去重&#xff0c;比如存入两个1或两个"123"&#xff0c;只有1条数据会存入成功&#xff0c;但有个特殊情况&#xff0c;如果添加到set的值是引用类型&#xff0c;比如数组…

DataGear 4.5.1 发布,数据可视化分析平台

DataGear 4.5.1 发布&#xff0c;严重 BUG 修复&#xff0c;具体更新内容如下&#xff1a; 修复&#xff1a;修复SQL数据集对于DB2、SQLite等数据源预览时会报错的BUG&#xff1b;修复&#xff1a;修复系统对于MySQL、MariaDB等数据源中无符号数值类型有时报错的BUG&#xff1…

借助媛如意让ROS机器人turtlesim画出美丽的曲线-云课版本

首先安装并打开猿如意其次打开蓝桥云课ROS并加入课程在猿如意输入问题得到答案在蓝桥云课ROS验证如何通过turtlesim入门ROS机器人您可以通过以下步骤入门ROS机器人&#xff1a;安装ROS&#xff1a;您需要安装ROS&#xff0c;可以在ROS官网上找到安装指南。安装turtlesim&#x…

英文拼写检查:TX Spell .NET for .NET 10.0 Crack

用于 Windows 窗体应用程序的 TX Text Control .NET 的强大拼写检查和语言工具。 表现 可靠准确的拼写检查 使用 TX Spell .NET for Windows Forms&#xff0c;您可以为基于 TX Text Control 的应用程序添加极其快速、高度可靠和非常准确的拼写检查。将 TX Spell .NET for Wind…

mysql中的共享锁,排他锁,间隙锁,意向锁及死锁机制

一、前言&#xff08;以下均为读完 高性能Mysql第四版 后的个人理解&#xff0c;建议阅读&#xff0c;挺不错的&#xff09;在写锁机制前先简单贴出mysql InnoDB引擎中的事务特性与隔离级别&#xff1a;事务的ACID标准(1)原子性-atomicity&#xff1a;一个事务作为一个不可分割…

vue中使用富文本Tinymce

本文是直接引用vue-element-admin中的&#xff0c;在此记录方便下次使用&#xff0c;日后再详细注释。 再src下的components下创建Tinymce 下包含以下文件 index.vue是主体文件 plugins.js 是 插件配置 toolbar.js 是 粗体、斜体等配置 EditorImage.vue 是右上角的上传 封装后…

一文速学-GBDT模型算法原理以及实现+Python项目实战

目录 前言 一、GBDT算法概述 1.决策树 2.Boosting 3.梯度提升 使用梯度上升找到最佳参数 二、GBDT算法原理 1.计算原理 2.预测原理 三、实例算法实现 1.模型训练阶段 1&#xff09;初始化弱学习器 2&#xff09;对于建立M棵分类回归树​&#xff1a; 四、Python实现 …

Spring_让Spring 依赖注入彻底废掉

在Spring之基于注解方式实例化BeanDefinition&#xff08;1&#xff09;_chen_yao_kerr的博客-CSDN博客中&#xff0c;我们在末尾处分享了一个甜点&#xff0c;就是关于实现了BeanDefinitionRegistryPostProcessor也可以实例化bean的操作&#xff0c;首先需要去了解一下那篇博客…

宝塔(二):升级JDK版本

目录 背景 一、下载JDK17 二、配置环境变量 三、配置新的JDK路径 背景 宝塔的软件商店只有JDK8&#xff0c;不满足我当前项目所需的JDK版本&#xff0c;因此想对JDK版本进行升级&#xff0c;升级为JDK17。 一、下载JDK17 先进入 /usr/lib/jvm 目录 点击终端&#xff0c;进…

OpenCV——line、circle、rectangle、ellipse、polylines函数的使用和绘制文本putText函数以及绘制中文的方法。

学习OpenCV的过程中&#xff0c;画图是不可避免的&#xff0c;本篇文章旨在介绍OpenCV中与画图相关的基础函数。 1、画线条——line()函数 介绍&#xff1a; cv2.line(image, start_point, end_point, color, thickness)参数&#xff1a; image: 图像start_point&#xff1a…

拉链表(小记)

拉链表创建外部表将编写的orders.txt上传到hdfs创建一个增减分区表将orders表的数据传入ods_orders_inc查看分区创建历史表插入数据操作创建外部表 create database lalian; use lalian;create external table orders(orderId int,createDate string,modifiedTime string,stat…

Redis集群方案应该怎么做?

今天我们来跟大家唠一唠JAVA核心技术-RedisRedis是一款流行的内存数据库&#xff0c;适用于高性能的数据缓存和实时数据处理。当需要处理大量数据时&#xff0c;可以使用Redis集群来提高性能和可用性。Redis在单节点模式下&#xff0c;虽然可以支持高并发、快速读写、丰富的数据…

sizeof与一维数组和二维数组

&#x1f355;博客主页&#xff1a;️自信不孤单 &#x1f36c;文章专栏&#xff1a;C语言 &#x1f35a;代码仓库&#xff1a;破浪晓梦 &#x1f36d;欢迎关注&#xff1a;欢迎大家点赞收藏关注 sizeof与一维数组和二维数组 文章目录sizeof与一维数组和二维数组前言1. sizeof与…

专业版即将支持自定义场景测试

物联网 MQTT 测试云服务 XMeter Cloud 专业版于 2022 年底上线后&#xff0c;已有不少用户试用&#xff0c;对数千甚至上万规模的 MQTT 并发连接和消息吞吐场景进行测试。同时我们也收到了希望支持更多物联网协议测试的需求反馈。 新年伊始&#xff0c;XMeter 团队全力聚焦于 …

搭建Gerrit环境Ubuntu

搭建Gerrit环境 1.安装apache sudo apt-get install apache2 注意:To run Gerrit behind an Apache server using mod_proxy, enable the necessary Apache2 modules: 执行:sudo a2enmod proxy_http 执行:sudo a2enmod ssl 使新的配置生效&#xff0c;需要执行如下命令:serv…