首页 > 数据库 >一次 Redis 事务使用不当引发的生产事故

一次 Redis 事务使用不当引发的生产事故

时间:2022-12-19 15:04:24浏览次数:72  
标签:使用不当 事务 null 事故 Transactional Redis 执行 递增

一次 Redis 事务使用不当引发的生产事故_微服务

这是悟空的第 170 篇原创文章

官网:​​http://www.passjava.cn​

你好,我是悟空。

本文主要内容如下:

一次 Redis 事务使用不当引发的生产事故_重启_02

一、前言

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

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

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

return redisTemplate.opsForValue().increment("count", 1);

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

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

二、排查

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

2.1 推测一

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

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

2.2 推测二

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

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

一次 Redis 事务使用不当引发的生产事故_微服务_03

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

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

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

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

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

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

2.3 验证推测二

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

一次 Redis 事务使用不当引发的生产事故_微服务_04

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

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

一次 Redis 事务使用不当引发的生产事故_Redis_05

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

一次 Redis 事务使用不当引发的生产事故_Redis_06

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

一次 Redis 事务使用不当引发的生产事故_重启_07

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

一次 Redis 事务使用不当引发的生产事故_Redis_08

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

一次 Redis 事务使用不当引发的生产事故_Redis_09

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

2.4 推测三

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

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

一次 Redis 事务使用不当引发的生产事故_重启_10

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

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

一次 Redis 事务使用不当引发的生产事故_微服务_11

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

2.5 验证推测三

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

一次 Redis 事务使用不当引发的生产事故_微服务_12

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

  • 先开启 Redis 事务支持,然后执行 Redis 的事务命令 multi 和 exec 。
  • 验证场景 3:在 @Transactional 注解的方法中执行 Redis 的递增操作。
  • 验证场景 4:在非 @Transactional 注解的方法中执行 Redis 的递增操作

2.5.1 执行 Redis 事务

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

一次 Redis 事务使用不当引发的生产事故_Redis_13

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

一次 Redis 事务使用不当引发的生产事故_微服务_14

2.5.2 @Transactional 中执行 Redis 命令

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

一次 Redis 事务使用不当引发的生产事故_重启_15

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

一次 Redis 事务使用不当引发的生产事故_Redis_16

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

一次 Redis 事务使用不当引发的生产事故_微服务_17

一次 Redis 事务使用不当引发的生产事故_微服务_18

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

一次 Redis 事务使用不当引发的生产事故_重启_19

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

一次 Redis 事务使用不当引发的生产事故_微服务_20

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

一次 Redis 事务使用不当引发的生产事故_微服务_21

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

三、源码解析

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

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

一次 Redis 事务使用不当引发的生产事故_微服务_22

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

一次 Redis 事务使用不当引发的生产事故_微服务_23

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

关键代码:conn.multi();

一次 Redis 事务使用不当引发的生产事故_微服务_24

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

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

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

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

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

四、修复方案

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

  • 方案一:每次 Redis 的事务操作完成后,关闭 Redis 事务支持,然后再执行 @Transactional 中的 Redis 命令。(有弊端
  • 方案二:创建两个 StringRedisTemplate,一个专门用来执行 Redis 事务,一个用来执行普通的 Redis 命令。

4.1 方案一

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

一次 Redis 事务使用不当引发的生产事故_Redis_25

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

一次 Redis 事务使用不当引发的生产事故_重启_26

4.2 方案二

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

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

代码如下所示:

一次 Redis 事务使用不当引发的生产事故_微服务_27

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

一次 Redis 事务使用不当引发的生产事故_微服务_28

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

一次 Redis 事务使用不当引发的生产事故_重启_29

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

一次 Redis 事务使用不当引发的生产事故_微服务_30

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

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

另外关于 Redis 事务使用还有一个坑,就是 Redis 连接未释放,导致获取不到连接了,这是下一个话题了~

- END -

关于我

8 年互联网开发经验,擅长微服务、分布式、架构设计。目前在一家大型上市公司从事基础架构和性能优化工作。

InfoQ 签约作者、蓝桥签约作者、阿里云专家博主、51CTO 红人。



标签:使用不当,事务,null,事故,Transactional,Redis,执行,递增
From: https://blog.51cto.com/u_11950846/5952650

相关文章

  • 【redis-01】linux与windows的远程互连
    写在开头博主在用linux远程连接windows上的redis时遇到了一些问题,网络上能搜索到的回答跟自己的情况不太相符,索性就总结一下相关问题写篇随笔分享给同路人,也方便以后自己......
  • docker-compose入门以及部署SpringBoot+Vue+Redis+Mysql(前后端分离项目)以若依前后端
    场景若依前后端分离版手把手教你本地搭建环境并运行项目:https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662上面在搭建起来前后端分离版的项目后。......
  • redis
    Redis高可用方案......
  • Redis
    Redis1.Redis概述Redis是一种基于键值对(key-value)的NoSQL数据库,是由C语言编写。1.1Redis特性速度快所有数据都是存放在内存中的。Redis是用C语言实现的,执行速度......
  • Redis——01 学习
    Redis——01主要学习目标:Redis的特点以及使用场景Redis单机版Redis常用命令~持久化策略~主从复制哨兵集群JedisSpringDataRedisRedis的特点以及使用场......
  • Redis——02 学习
    Redis——02前面了解了Redis以及在Linux的安装,下面了解一些Redis常用的命令。Redis常用命令:Redis是Key-Value形式,Key为字符串类型,而Value的取值类型如下:......
  • Redis——03 学习
    Redis——03Redis持久化策略Redis不仅仅是一个内存型数据库,还具备持久化能力。这个持久化并不是Redis数据库读写的主要内容,跟MySQL不一样,这个持久化只是为了备份,......
  • Redis——05 学习
    Redis——05之前了解了主从复制以及哨兵,接下来了解集群模式。集群(Cluster)先来聊一下前面学的哨兵Sentinel,一般利用哨兵对master节点继续监控,如果master发生异常,则......
  • Redis——04 学习
    Redis——04之前讲了redis的主从复制模型的优点以及搭建效果,但是依然有自己的缺陷,如果主节点宕机,那么其他从节点可能还有运行的,如果此时任意一个从节点升级生了主节点那......
  • Redis——06 学习
    Redis——06将Redis的基本使用以及三种模式进行了学习和了解。接下来就学习如何在Java中以及SpringBoot框架中使用Redis。JedisRedis在Java上的操作多半是集......