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


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

文章插图
这是悟空的第 170 篇原创文章
官网:http://www.passjava.cn
你好,我是悟空 。
本文主要内容如下:
一次 Redis 事务使用不当引发的生产事故

文章插图
一、前言最近项目的生产环境遇到一个奇怪的问题:
现象:每天早上客服人员在后台创建客服事件时,都会创建失败 。当我们重启这个微服务后,后台就可以正常创建了客服事件了 。到第二天早上又会创建失败,又得重启这个微服务才行 。
初步排查:创建一个客服事件时 , 会用到 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 设计与实现)
继续看代码,发现在操作 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 的时候用到事务 。我将这位同事加的代码简化后如下所示:

推荐阅读