一次 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 方法 。

经验总结扩展阅读