0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Redis 递增修复操作

jf_ro2CN3Fa 来源:芋道源码 2023-06-14 09:47 次阅读

一、前言

二、排查

三、源码解析

四、修复方案

一、前言

最近项目的生产环境遇到一个奇怪的问题:

现象 :每天早上客服人员在后台创建客服事件时,都会创建失败 。当我们重启 这个微服务后,后台就可以正常创建了客服事件了。到第二天早上又会创建失败,又得重启这个微服务才行。

初步排查 :创建一个客服事件时,会用到 Redis 的递增操作来生成一个唯一的分布式 ID 作为事件 id。代码如下所示:

returnredisTemplate.opsForValue().increment("count",1);

而恰巧每天早上这个递增操作都会返回 null,进而导致后面的一系列逻辑出错,保存客服事件失败。当重启微服务后,这个递增操作又正常了。

那么排查的方向就是 Redis 的操作为什么会返回 null 了,以及为什么重启就又恢复正常了。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

视频教程:https://doc.iocoder.cn/video/

二、排查

根据上面的信息,我们先来看看 Redis 的自增操作在什么情况下会返回 null。

2.1 推测一

根据重启后就恢复正常,我们推测晚上执行了大量的 job,大量 Redis 连接未释放,当早上再来执行 Redis 操作时,执行失败。重启后,连接自动释放了。

但是其他有使用到 Redis 的业务功能又是正常的,所以推测一的方向有问题,排除

2.2 推测二

可能是 Redis 事务造成的问题。这个推测的依据是根据下面的代码来排查的。

直接看 redisTemplate 递增的方法 increment,如下所示:

7757a1ba-0a53-11ee-962d-dac502259ad0.png

官方注释已经说明什么情况下会返回 null:

当在 pipeline(管道)中使用这个 increment 方法时会返回 null。

当在 transaction(事务)中使用这个 increment 方法时会返回 null。

事务 提供了一种将多个命令打包,然后一次性、有序地执行机制.

多个命令会被入列到事务队列中,然后按先进先出(FIFO)的顺序执行。

事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。(内容来自 Redis 设计与实现)

继续看代码,发现在操作 Redis 的 ServiceImpl 实现类的上面添加了一个 @Transactional 注解,推测是不是这个注解影响了 Redis 的操作结果。

2.3 验证推测二

如下面的表格所示,第二行中没有添加 Spring 的事务注解 @Transactional时,执行 Redis 的递增命令肯定是正常的,而接下来要验证的是表格中的第一行:加了 @Transactional 是否对 Redis 的命令有影响。

77660b60-0a53-11ee-962d-dac502259ad0.png

为了验证上面的推论,我写了一个 Demo 程序。

Controller 类 ,定义了一个 API,用来模拟前端发起的请求:

777f4a12-0a53-11ee-962d-dac502259ad0.png

Service 实现类 ,定义了一个方法,用来递增 Redis 中的 count 键,每次递增 1,然后返回命令执行后的结果。而且这个 Service 方法加了@Transactional 注解。

779b3696-0a53-11ee-962d-dac502259ad0.png

Postman 测试下,发现每发一次请求,count 都会递增 1,并没有返回 null。

77ac9562-0a53-11ee-962d-dac502259ad0.png

然后到 Redis 中查看数据,count 的值也是递增后的值 38,也不是 null。

77d27232-0a53-11ee-962d-dac502259ad0.png

通过这个实验说明在 @Transactional 注解的方法里面执行 Redis 的操作并不会返回 null,结论我记录到了表格中。

77d83820-0a53-11ee-962d-dac502259ad0.png

所以说上面的推论不成立(加了 @Transactional 注解并不影响),到这里线索似乎断了

2.4 推测三

然后跟当时做这块功能的开发人员说明了情况,告诉他可能是 Redis 事务造成的,然后问有没有其他同学在凌晨执行过 Redis 事务相关的 Job。

他说最近有同事加过 Redis 的事务功能,在凌晨执行 Job 的时候用到事务。我将这位同事加的代码简化后如下所示:

77f786e4-0a53-11ee-962d-dac502259ad0.png

下面是针对这段代码的解释,简单来说就是开启事务,将 Redis 命令顺序放到一个队列中,然后最后一起执行,且保证原子性。

setEnableTransactionSupport表示是否开启事务支持,默认不开启。

782042f0-0a53-11ee-962d-dac502259ad0.png

难道开启了 Redis 事务,还能影响 Spring 事务中的 Redis 操作?

2.5 验证推测三

如下表,序号 3 和 序号 4 的场景都是开启了 Redis 的事务支持 ,两个场景的区别是是否加了 @Transactional 注解

78389c42-0a53-11ee-962d-dac502259ad0.png

为了验证上面的场景,我们来做个实验:

先开启 Redis 事务支持,然后执行 Redis 的事务命令 multi 和 exec 。

验证场景 3:在 @Transactional 注解的方法中执行 Redis 的递增操作。

验证场景 4:在非 @Transactional 注解的方法中执行 Redis 的递增操作

2.5.1 执行 Redis 事务

首先就用 Redis 的 multi 和 exec 命令来设置两个 key 的值。

785b039a-0a53-11ee-962d-dac502259ad0.png

如下图所示,设置成功了。

7871127a-0a53-11ee-962d-dac502259ad0.png

2.5.2 @Transactional 中执行 Redis 命令

接下来在标注有 @Transactional 注解的方法中执行 Redis 的递增操作。

78a66524-0a53-11ee-962d-dac502259ad0.png

多次执行这个命令返回的结果都是 null,这不就正好重现了!

78d62e30-0a53-11ee-962d-dac502259ad0.png

再来看 Redis 中 count 的值,发现每执行一次 API 请求调用,都会递增 1,所以虽然命令返回的是 null,但最后 Redis 中存放的还是递增后的结果。

78e95b5e-0a53-11ee-962d-dac502259ad0.png78f7d882-0a53-11ee-962d-dac502259ad0.png

接下来我们验证下场景 4,先执行 Redis 事务操作,然后在不添加 @Transactional 注解的方法中执行 Redis 递增操作。

790e7bc8-0a53-11ee-962d-dac502259ad0.png

用 Postman 调用这个接口后,正常返回自增后的结果,并不是返回 null。说明在非 @Transactional 中执行 Redis 操作并没有受到 Redis 事务的影响。

7932b3e4-0a53-11ee-962d-dac502259ad0.png

四个场景的结论如下所示,只有第三个场景下,Redis 的递增操作才会返回 null。

795da496-0a53-11ee-962d-dac502259ad0.png

问题原因找到了,说明 RedisTemplete 开启了 Redis 事务支持后,在 @Transactional 中执行的 Redis 命令也会被认为是在 Redis 事务中执行的,要执行的递增命令会被放到队列中,不会立即返回执行后的结果,返回的是一个 null,需要等待事务提交时,队列中的命令才会顺序执行,最后 Redis 数据库的键值才会递增。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/yudao-cloud

视频教程:https://doc.iocoder.cn/video/

三、源码解析

那我们就看下为什么开启了 Redis 事务支持,效果就不一样了。

找到 Redis 执行命令的核心方法, execute 方法。

7972aeae-0a53-11ee-962d-dac502259ad0.png

然后一步一步点进去看,关键代码就是 211 行到 216 行,有一个逻辑判断,当开启了 Redis 事务支持后,就会去绑定一个连接(bindConnection),否则就去获取新的 Redis 连接(getConnection)。这里我们是开启了的,所以再到 bindConnection方法中查看如何绑定连接的。

79951e62-0a53-11ee-962d-dac502259ad0.png

接着往下看,关键代码如下所示,当开启了 Redis 事务支持,且添加了 @Transactional 注解时,就会执行 Redis 的 mutil 命令。

关键代码:conn.multi();

79b52cc0-0a53-11ee-962d-dac502259ad0.png

Redis Multi 命令 用于标记一个事务块的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。

真相大白,开启 Redis 事务支持 + @Transactional 注解后,最后其实是标记了一个 Redis 事务块,后续的操作命令是在这个事务块中执行的。

比如下面的的递增命令并不会返回递增后的结果,而是返回 null。

stringRedisTemplate.opsForValue().increment("count",1);

而我们的生产环境重启服务后,开启的 Redis 事务支持又被重置为默认值了,所以后续的 Redis 递增操作都能正常执行。

四、修复方案

目前想到了两种解决方案:

方案一:每次 Redis 的事务操作完成后,关闭 Redis 事务支持,然后再执行 @Transactional 中的 Redis 命令。(有弊端

方案二:创建两个 StringRedisTemplate,一个专门用来执行 Redis 事务,一个用来执行普通的 Redis 命令。

4.1 方案一

方案一的写法如下,先开启事务支持,事务执行之后,再关闭事务支持。

7a0274bc-0a53-11ee-962d-dac502259ad0.png

但是这种写法有个弊端 ,如果在执行 Redis 事务期间,在 @Transactional 注解的方法里面执行 Redis 命令,则还是会造成返回结果为 null。

7a2d12e4-0a53-11ee-962d-dac502259ad0.png

4.2 方案二

弄两个 RedisTemplate Bean,一个是用来执行 Redis 事务的,一个是用来执行普通 Redis 命令的(不支持事务)。不同的地方引入不同的 Bean 就可以了。

先创建一个 RedisConfig 文件,自动装配两个 Bean。一个 Bean 名为 stringRedisTemplate 代表不支持事务的,执行命令后立即返回实际的执行结果。另外一个 Bean 名为 stringRedisTemplateTransaction,代表开启 Redis 事务支持的。

代码如下所示:

7a427f76-0a53-11ee-962d-dac502259ad0.png

接下来在测试的 Service 类中注入两个不同的 StringRedisTemplate 实例,代码如下所示:

7a5d9518-0a53-11ee-962d-dac502259ad0.png

Redis 事务的操作改写成这样,且不需要手动开启 Redis 事务支持了。用到的 StringRedisTemplate 是支持事务的那个实例。

7a7a695e-0a53-11ee-962d-dac502259ad0.png

在 Spring 的 @Tranactional 中执行的 Redis 命令如下所示,用到的 StringRedisTemplate 是不支持事务的那个实例。

7ac18d98-0a53-11ee-962d-dac502259ad0.png

然后还是按照上面场景 3 的测试步骤,先执行 testRedisMutil 方法,再执行 testTransactionAnnotations 方法。

验证结果 :Redis 递增操作正常返回 count 的值,修复完成。

责任编辑:彭菁

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 源码
    +关注

    关注

    8

    文章

    633

    浏览量

    29138
  • 小程序
    +关注

    关注

    1

    文章

    234

    浏览量

    12110
  • Redis
    +关注

    关注

    0

    文章

    371

    浏览量

    10845

原文标题:当 Redis 碰上 @Transactional,有大坑,要注意!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何使用Rust连接Redis

    Rust操作RedisRedis依赖库 在Rust中有很多Redis的客户端库可以选择,这里我们选择使用redis-rs库。在Cargo
    的头像 发表于 09-19 16:22 2220次阅读

    Redis Stream应用案例

    摘要: Redis Stream Redis最新的大版本5.0已经RC1了,其中最重要的Feature莫过于Redis Stream了,关于Redis Stream的基本使用介绍和设计
    发表于 06-26 17:15

    laravel使用redis

    laravel操作redis笔记!
    发表于 09-24 09:40

    Redis的安装和使用步骤

    Python操作Redis之安装和使用(一)
    发表于 09-29 09:29

    labview读写操作REDIS

    本帖最后由 SevenLi8408 于 2022-9-15 08:07 编辑 分享一个好用的非关系型缓存数据库的使用方法。REDIS桌面管理软件https://github.com
    发表于 08-15 10:32

    电池修复的基础操作

    电池修复的基础操作 在这里介绍的电动车蓄/电池修复/之基础操作步骤,就是指常规使用的修复方法,修复
    发表于 11-10 13:44 1041次阅读

    python操作redis

    --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,
    发表于 11-28 11:02 748次阅读
    python<b class='flag-5'>操作</b><b class='flag-5'>redis</b>

    基于多线程环境下值的递增操作--原子操作

    因此在多线程环境中对一个变量进行读写时,我们需要有一种方法能够保证对一个值的递增操作是原子操作——即不可打断性,一个线程在执行原子操作时,其它线程必须等待它完成之后才能开始执行该原子
    的头像 发表于 01-10 11:16 6143次阅读
    基于多线程环境下值的<b class='flag-5'>递增</b><b class='flag-5'>操作</b>--原子<b class='flag-5'>操作</b>

    Springboot+redis操作多种实现

    一、Jedis,Redisson,Lettuce三者的区别共同点:都提供了基于Redis操作的Java API,只是封装程度,具体实现稍有不同。 不同点: 1.1、Jedis 是Redis的Java
    的头像 发表于 09-22 10:48 1796次阅读
    Springboot+<b class='flag-5'>redis</b><b class='flag-5'>操作</b>多种实现

    mysql_redis在MySQL中操作Redis 

    ./oschina_soft/gitee-mysql_redis.zip
    发表于 06-22 14:35 2次下载
    mysql_<b class='flag-5'>redis</b>在MySQL中<b class='flag-5'>操作</b><b class='flag-5'>Redis</b> 

    Redis数据同步解决方案—NineData

    NineData(https://www.ninedata.cloud/)在Redis的同步上,提供了稳定和高效的解决方案,并且性能上也领先其他同步工具,特别是在同步的动态限流、数据对比修复和限流
    的头像 发表于 06-05 15:31 802次阅读
    <b class='flag-5'>Redis</b>数据同步解决方案—NineData

    Redis是什么?简述它的优缺点?

    Redis是什么?简述它的优缺点? Redis本质上是一个Key-Value类型的内存数据库,很像Memcached,整个数据库加载在内存当中操作,定期通过异步操作把数据库中的数据fl
    的头像 发表于 10-09 10:37 789次阅读

    redis的increment方法

    实现对存储在数据库中的特定键的递增操作。在本文中,我们将详细介绍Redis的 INCR 方法,包括其原理、使用方法以及一些常见的应用场景。 首先,我们来看看Redis的 INCR 方法
    的头像 发表于 12-05 09:57 1166次阅读

    redis的主要方法

    Redis是一种基于内存的开源键值对存储系统,常用于缓存、消息中间件、数据库等场景。作为一个高性能的NoSQL存储解决方案,Redis提供了丰富的方法用于操作数据。本文将详细介绍Redis
    的头像 发表于 12-05 09:59 783次阅读

    redis使用多线程处理操作命令

    Redis 是一个使用多线程处理操作命令的开源内存数据库系统。它以其高性能、可扩展性和灵活性而闻名,通常被用作缓存、消息代理和数据存储等各种应用场景。在本文中,我们将详尽、详实、细致地探
    的头像 发表于 12-05 10:25 545次阅读