文章 62
浏览 15135
当 Redis 碰上 @Transactional,有大坑

当 Redis 碰上 @Transactional,有大坑

标题:
探索 Redis 与 @Transactional 注解的冲突之谜

image.png

正文:
前言
在我们的项目中,我们遇到了一个让人费解的生产环境问题:每天早上,当运营人员后台尝试创建新事件时,系统总是无法成功创建。经过重新启动相关微服务后,系统又能恢复正常运行,直到次日早上问题再次出现,又得重启服务。

初步调查
我们发现,问题出现在使用 Redis 生成唯一分布式 ID 的过程中。每天早上,Redis 的递增操作异常地返回了 null 值,导致后续流程中断。重新启动服务似乎是暂时的解决方法,但根本问题仍然悬而未决。

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

深入探索

  • 根据重启后就恢复正常,我们推测晚上执行了大量的 job,大量 Redis 连接未释放,当早上再来执行 Redis 操作时,执行失败。重启后,连接自动释放了。但是其他有使用到 Redis 的业务功能又是正常的,所以推测一的方向有问题,排除

  • 通过查询 ```redisTemplate 递增的方法 increment` 源码注释,

    image.png

    发现事务&管道会返回 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 方法。

image.png

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

image.png

image.png

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

解决方案
针对这一问题,我们制定了两种修复方案:

  1. 在 Redis 事务操作完成后关闭 Redis 事务支持,然后进行 @Transactional 注解方法中的 Redis 操作。

  2. 创建两个 StringRedisTemplate,一个专用于事务操作,另一个用于非事务操作。

    image.png

我们选择了第二种方案。通过配置两个不同的 StringRedisTemplate 实例,我们成功隔离了事务与非事务操作,彻底解决了问题。


标题:当 Redis 碰上 @Transactional,有大坑
作者:xiaohugg
地址:https://xiaohugg.top/articles/2024/01/17/1705460384079.html

人民有信仰 民族有希望 国家有力量