我正在参加「启航计划」

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

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

你好,我是悟空。

本文主要内容如下:

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

文章已同步至:www.passjava.cn

一、前言

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

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

初步排查:创立一个客服事情时,会用到 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 事务使用不当引发的生产事故

官方注释已经阐明什么状况下会回来 null:

  • 当在 pipeline(管道)中运用这个 increment 办法时会回来 null。
  • 当在 transaction(业务)中运用这个 increment 办法时会回来 null。

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

多个指令会被入列到业务行列中,然后按先进先出(FIFO)的顺序履行。

业务在履行过程中不会被中断,当业务行列中的所有指令都被履行结束之后,业务才会结束。(内容来自 Redis 规划与完成)

持续看代码,发现在操作 Redis 的 ServiceImpl 完成类的上面增加了一个 @Transactional 注解,估测是不是这个注解影响了 Redis 的操作成果。

2.3 验证估测二

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

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

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

Controller 类,界说了一个 API,用来模仿前端发起的请求:

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

Service 完成类,界说了一个办法,用来递加 Redis 中的 count 键,每次递加 1,然后回来指令履行后的成果。而且这个 Service 办法加了@Transactional 注解。

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

Postman 测验下,发现每发一次请求,count 都会递加 1,并没有回来 null。

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

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

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

经过这个试验阐明在 @Transactional 注解的办法里边履行 Redis 的操作并不会回来 null,定论我记载到了表格中。

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

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

2.4 估测三

然后跟其时做这块功用的开发人员阐明了状况,告知他可能是 Redis 业务形成的,然后问有没有其他同学在清晨履行过 Redis 业务相关的 Job。

他说最近有搭档加过 Redis 的业务功用,在清晨履行 Job 的时分用到业务。我将这位搭档加的代码简化后如下所示:

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

下面是针对这段代码的解说,简单来说便是敞开业务,将 Redis 指令顺序放到一个行列中,然后最终一同履行,且保证原子性。

setEnableTransactionSupport表明是否敞开业务支撑,默许不敞开。

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

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

2.5 验证估测三

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

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

为了验证上面的场景,咱们来做个试验:

  • 先敞开 Redis 业务支撑,然后履行 Redis 的业务指令 multi 和 exec 。
  • 验证场景 3:在 @Transactional 注解的办法中履行 Redis 的递加操作。
  • 验证场景 4:在非 @Transactional 注解的办法中履行 Redis 的递加操作

2.5.1 履行 Redis 业务

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

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

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

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

2.5.2 @Transactional 中履行 Redis 指令

接下来在标注有 @Transactional 注解的办法中履行 Redis 的递加操作。

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

多次履行这个指令回来的成果都是 null,这不就正好重现了!

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

再来看 Redis 中 count 的值,发现每履行一次 API 请求调用,都会递加 1,所以虽然指令回来的是 null,但最终 Redis 中寄存的仍是递加后的成果。

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

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

接下来咱们验证下场景 4,先履行 Redis 业务操作,然后在不增加 @Transactional 注解的办法中履行 Redis 递加操作。

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

用 Postman 调用这个接口后,正常回来自增后的成果,并不是回来 null。阐明在非 @Transactional 中履行 Redis 操作并没有受到 Redis 业务的影响。

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

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

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

问题原因找到了,阐明 RedisTemplete 敞开了 Redis 业务支撑后,在 @Transactional 中履行的 Redis 指令也会被认为是在 Redis 业务中履行的,要履行的递加指令会被放到行列中,不会当即回来履行后的成果,回来的是一个 null,需求等待业务提交时,行列中的指令才会顺序履行,最终 Redis 数据库的键值才会递加。

三、源码解析

那咱们就看下为什么敞开了 Redis 业务支撑,效果就不相同了。

找到 Redis 履行指令的中心办法, execute 办法。

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

然后一步一步点进去看,要害代码便是 211 行到 216 行,有一个逻辑判别,当敞开了 Redis 业务支撑后,就会去绑定一个衔接(bindConnection),否则就去获取新的 Redis 衔接(getConnection)。这儿咱们是敞开了的,所以再到 bindConnection办法中检查怎么绑定衔接的。

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

接着往下看,要害代码如下所示,当敞开了 Redis 业务支撑,且增加了 @Transactional 注解时,就会履行 Redis 的 mutil 指令。

要害代码:conn.multi();

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

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 业务期间,在 @Transactional 注解的办法里边履行 Redis 指令,则仍是会形成回来成果为 null。

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

4.2 计划二

弄两个 RedisTemplate Bean,一个是用来履行 Redis 业务的,一个是用来履行普通 Redis 指令的(不支撑业务)。不同的地方引进不同的 Bean 就可以了。

先创立一个 RedisConfig 文件,自动装配两个 Bean。一个 Bean 名为 stringRedisTemplate 代表不支撑业务的,履行指令后当即回来实践的履行成果。别的一个 Bean 名为 stringRedisTemplateTransaction,代表敞开 Redis 业务支撑的。

代码如下所示:

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

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

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

Redis 业务的操作改写成这样,且不需求手动敞开 Redis 业务支撑了。用到的 StringRedisTemplate 是支撑业务的那个实例。

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

在 Spring 的 @Tranactional 中履行的 Redis 指令如下所示,用到的 StringRedisTemplate 是不支撑业务的那个实例。

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

然后仍是依照上面场景 3 的测验步骤,先履行 testRedisMutil 办法,再履行 testTransactionAnnotations 办法。

验证成果:Redis 递加操作正常回来 count 的值,修正完成。

别的关于 Redis 业务运用还有一个坑,便是 Redis 衔接未释放,导致获取不到衔接了,这是下一个话题了~

参考资料:blog.csdn.net/qq_34021712…

– END –

关于我

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

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