一、前言最近项目的生产环境遇到一个奇怪的问题: 现象 :每天早上客服人员在后台创建客服事件时,都会创建失败 。当我们重启 这个微服务后,后台就可以正常创建了客服事件了。到第二天早上又会创建失败,又得重启这个微服务才行。 初步排查 :创建一个客服事件时,会用到 Redis 的递增操作来生成一个唯一的分布式 ID 作为事件 id。代码如下所示:
而恰巧每天早上这个递增操作都会返回
二、排查根据上面的信息,我们先来看看 Redis 的自增操作在什么情况下会返回 null。 2.1 推测一根据重启后就恢复正常,我们推测晚上执行了大量的 job,大量 Redis 连接未释放,当早上再来执行 Redis 操作时,执行失败。重启后,连接自动释放了。 但是其他有使用到 Redis 的业务功能又是正常的,所以推测一的方向有问题,排除 。 2.2 推测二可能是 Redis 事务造成的问题。这个推测的依据是根据下面的代码来排查的。 直接看 ![]() 官方注释已经说明什么情况下会返回 null:
继续看代码,发现在操作 Redis 的 ServiceImpl 实现类的上面添加了一个 @Transactional 注解,推测是不是这个注解影响了 Redis 的操作结果。 2.3 验证推测二如下面的表格所示,第二行中没有添加 Spring 的事务注解 ![]() 为了验证上面的推论,我写了一个 Demo 程序。 Controller 类 ,定义了一个 API,用来模拟前端发起的请求: ![]() Service 实现类 ,定义了一个方法,用来递增 Redis 中的 count 键,每次递增 1,然后返回命令执行后的结果。而且这个 Service 方法加了@Transactional 注解。 ![]() Postman 测试下,发现每发一次请求,count 都会递增 1,并没有返回 null。 ![]() 然后到 Redis 中查看数据,count 的值也是递增后的值 38,也不是 null。 ![]() 通过这个实验说明在 @Transactional 注解的方法里面执行 Redis 的操作并不会返回 null,结论我记录到了表格中。 ![]() 所以说上面的推论不成立(加了 @Transactional 注解并不影响),到这里线索似乎断了 。 2.4 推测三然后跟当时做这块功能的开发人员说明了情况,告诉他可能是 Redis 事务造成的,然后问有没有其他同学在凌晨执行过 Redis 事务相关的 Job。 他说最近有同事加过 Redis 的事务功能,在凌晨执行 Job 的时候用到事务。我将这位同事加的代码简化后如下所示: ![]() 下面是针对这段代码的解释,简单来说就是开启事务,将 Redis 命令顺序放到一个队列中,然后最后一起执行,且保证原子性。
![]()
2.5 验证推测三如下表,序号 3 和 序号 4 的场景都是开启了 Redis 的事务支持 ,两个场景的区别是是否加了 @Transactional 注解 。 ![]() 为了验证上面的场景,我们来做个实验:
2.5.1 执行 Redis 事务首先就用 Redis 的 multi 和 exec 命令来设置两个 key 的值。 ![]() 如下图所示,设置成功了。 ![]() 2.5.2 @Transactional 中执行 Redis 命令接下来在标注有 @Transactional 注解的方法中执行 Redis 的递增操作。 ![]() 多次执行这个命令返回的结果都是 null,这不就正好重现了! ![]() 再来看 Redis 中 count 的值,发现每执行一次 API 请求调用,都会递增 1,所以虽然命令返回的是 null,但最后 Redis 中存放的还是递增后的结果。 ![]() ![]() 接下来我们验证下场景 4,先执行 Redis 事务操作,然后在不添加 @Transactional 注解的方法中执行 Redis 递增操作。 ![]() 用 Postman 调用这个接口后,正常返回自增后的结果,并不是返回 null。说明在非 @Transactional 中执行 Redis 操作并没有受到 Redis 事务的影响。 ![]() 四个场景的结论如下所示,只有第三个场景下,Redis 的递增操作才会返回 null。 ![]() 问题原因找到了,说明 RedisTemplete 开启了 Redis 事务支持后,在 @Transactional 中执行的 Redis 命令也会被认为是在 Redis 事务中执行的,要执行的递增命令会被放到队列中,不会立即返回执行后的结果,返回的是一个 null,需要等待事务提交时,队列中的命令才会顺序执行,最后 Redis 数据库的键值才会递增。
三、源码解析那我们就看下为什么开启了 Redis 事务支持,效果就不一样了。 找到 Redis 执行命令的核心方法, execute 方法。 ![]() 然后一步一步点进去看,关键代码就是 211 行到 216 行,有一个逻辑判断,当开启了 Redis 事务支持后,就会去绑定一个连接( ![]() 接着往下看,关键代码如下所示,当开启了 Redis 事务支持,且添加了 @Transactional 注解时,就会执行 Redis 的 mutil 命令。 关键代码:conn.multi(); ![]() Redis Multi 命令 用于标记一个事务块的开始,事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行。
比如下面的的递增命令并不会返回递增后的结果,而是返回 null。
而我们的生产环境重启服务后,开启的 Redis 事务支持又被重置为默认值了,所以后续的 Redis 递增操作都能正常执行。 四、修复方案目前想到了两种解决方案:
4.1 方案一方案一的写法如下,先开启事务支持,事务执行之后,再关闭事务支持。 ![]() 但是这种写法有个弊端 ,如果在执行 Redis 事务期间,在 @Transactional 注解的方法里面执行 Redis 命令,则还是会造成返回结果为 null。 ![]() 4.2 方案二弄两个 RedisTemplate Bean,一个是用来执行 Redis 事务的,一个是用来执行普通 Redis 命令的(不支持事务)。不同的地方引入不同的 Bean 就可以了。 先创建一个 RedisConfig 文件,自动装配两个 Bean。一个 Bean 名为 代码如下所示: ![]() 接下来在测试的 Service 类中注入两个不同的 StringRedisTemplate 实例,代码如下所示: ![]() Redis 事务的操作改写成这样,且不需要手动开启 Redis 事务支持了。用到的 StringRedisTemplate 是支持事务的那个实例。 ![]() 在 Spring 的 @Tranactional 中执行的 Redis 命令如下所示,用到的 StringRedisTemplate 是不支持事务的那个实例。 ![]() 然后还是按照上面场景 3 的测试步骤,先执行 testRedisMutil 方法,再执行 testTransactionAnnotations 方法。 验证结果 :Redis 递增操作正常返回 count 的值,修复完成。 |
|
来自: goldbomb > 《Spring Boot》