标题:
探索 Redis 与 @Transactional 注解的冲突之谜
正文:
前言
在我们的项目中,我们遇到了一个让人费解的生产环境问题:每天早上,当运营人员后台尝试创建新事件时,系统总是无法成功创建。经过重新启动相关微服务后,系统又能恢复正常运行,直到次日早上问题再次出现,又得重启服务。
初步调查
我们发现,问题出现在使用 Redis 生成唯一分布式 ID 的过程中。每天早上,Redis 的递增操作异常地返回了 null 值,导致后续流程中断。重新启动服务似乎是暂时的解决方法,但根本问题仍然悬而未决。
return redisTemplate.opsForValue().increment("count", 1);
深入探索
-
根据重启后就恢复正常,我们推测晚上执行了大量的 job,大量 Redis 连接未释放,当早上再来执行 Redis 操作时,执行失败。重启后,连接自动释放了。但是其他有使用到 Redis 的业务功能又是正常的,所以推测一的方向有问题,排除 。
-
通过查询 ```redisTemplate
递增的方法
increment` 源码注释,发现事务&管道会返回 null 的情况,基于这思路继续排查业务代码
通 过审查代码,我们意识到,在 ServiceImpl 上加上了 @Transactional 注解的方法运行中,调用 Redis 递增操作会返回 null。这引出了一个新的问题:开启了 Spring 的 @Transactional 注解,是否会影响 Redis 操作?
事务 提供了一种将多个命令打包,然后一次性、有序地执行机制.
多个命令会被入列到事务队列中,然后按先进先出(FIFO)的顺序执行。
事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。(内容来自 Redis 设计与实现)
实证测试
为了验证我们的猜想,我们设计了一系列测试。我们创建了一个简单的 API 来模拟客户端请求,并且在带有 @Transactional 注解的方法中执行 Redis 递增命令,结果并未如预期返回 null。
然而,当我们在开启 Redis 事务支持的情况下进行同样的操作,情况就不同了。通过多次测试,我们发现 @Transactional 注解的方法确实会在开启 Redis 事务支持时返回 null。
这一发现揭示了问题的本质:开启 Redis 事务支持会影响 Spring 的 @Transactional 注解方法中的 Redis 操作。
源码剖析
深入到 Redis 命令的执行机制,我们发现了关键的代码逻辑。当 Redis 事务支持被启用时,如果还有 @Transactional 注解,那么后续的 Redis 命令不会立即返回结果,而是被放入事务队列中。
找到 Redis 执行命令的核心方法, execute 方法。
然后一步一步点进去看,关键代码就是 211 行到 216 行,有一个逻辑判断,当开启了 Redis 事务支持后,就会去绑定一个连接(bindConnection
),否则就去获取新的 Redis 连接(getConnection
)。这里我们是开启了的,所以再到 bindConnection
方法中查看如何绑定连接的。
真相大白,开启 Redis 事务支持 + @Transactional 注解后,最后其实是标记了一个 Redis 事务块,后续的操作命令是在这个事务块中执行的。
解决方案
针对这一问题,我们制定了两种修复方案:
-
在 Redis 事务操作完成后关闭 Redis 事务支持,然后进行 @Transactional 注解方法中的 Redis 操作。
-
创建两个 StringRedisTemplate,一个专用于事务操作,另一个用于非事务操作。
我们选择了第二种方案。通过配置两个不同的 StringRedisTemplate 实例,我们成功隔离了事务与非事务操作,彻底解决了问题。