Transaction - 记一次 Spring 事务联合 Redis 挂了引发的生产事故

问题描述

java.lang.RuntimeException: java.lang.IllegalStateException: Already value [...数据源信息...] bound to thread [[Ljava.lang.String;@231b1ae0.container-0-C-1]

上述问题是本次问题的最终结果,但并不是问题主因,之所以会引发这个问题,还需要先梳理下,我们的业务流程:针对一次入库的数据进行更新维护,先删除后新增,并且开了多线程,那么问题来了......

原因分析

我们的针对多线程事务 + 多数据源设计思路

  1. 动态数据源方法
  2. 手动更新多线程集合事务方案
  3. 手动提交/回滚事务方案(多线程)
  4. 必须在 finally 里执行,保证一定会被执行到

因为本次 Redis 突然 OOM 被打挂了,导致业务流程突然报错中断,那么如果这时候已经进入了事务操作......因为牵扯到多线程事务处理,我们需要先执行第 2 步

java 复制代码
private void updateTranSyncManager(List<TransactionStatus> transactionStatusList) {
        // err: No value for key [DynamicRoutingDataSource] bound to thread
        TransactionSynchronizationManager.bindResource(dynamicRoutingDataSource, dynamicRoutingDataSource);
        // err: Transaction synchronization is not active
        TransactionSynchronizationManager.initSynchronization();
}

把本次要提交的数据源绑定到事务管理器里,使得在第 3 步的提交或者回滚事务的时候,可以处理好本次的数据源信息,至于为什么要这么做,问 Spring,因为源码里就是要这么操作的,所以这边要遵守这个规则。

java 复制代码
private boolean transactionHandle(List<TransactionStatus> transactionStatusList, AtomicBoolean isError) {
        // 事务统一提交/回滚检验
        if (!transactionStatusList.isEmpty()) {
            if (isError.get()) {
                // 有错误, 回滚
                log.info("事务回滚...");
                transactionManager.rollback(transactionStatusList.get(0));
                log.info("事务回滚成功...");
            } else {
                // 无错误, 提交
                log.info("事务提交...");
                transactionManager.commit(transactionStatusList.get(0));
                log.info("事务提交成功...");
                return true;
            }
        }
        return false;
}

Tips1:顺便提一嘴,为什么这个是使用 get(0) 而不是整个 List 扔进去呢,因为看源码发现,因为我们业务是在同一个数据源里操作,所以只需要提交第一个即可,虽然 List 因为业务因素会添加很多,猜想里面数据源是引用对象,所以会自动处理相等对象的逻辑

Tip2:非常关键,updateTranSyncManager & transactionHandle 这 2 个方法必须保持一致性,怎么理解呢?就是 bindResource 后必须要执行 commit 或 rollback 方法,否则就会引起不一致性,导致上面的报错,要么就 2 个方法都不要执行,也是一种一致性

好了,精彩的故事正式开始......

有了上面的基础概念后,我们看这个 Redis 如果挂了的话,比如此时正好在业务处理的时候报错了呢?那么很有可能 DELETE 和 INSERT 操作没做完,导致事务 transactionStatusList 没有执行 add 操作,那么就会到 updateTranSyncManager 这方法里进行 bindResource,但是呢,等到进入 transactionHandle 方法里的时候,因为 List 是空的,所以不会执行 commit 或 rollback 操作,那么就会违背我们上面的规则(没有达成一致性),我们也称为事务管理器里的数据源混乱现象。

Ps:transactionStatusList 这个的入口是在执行 INSERT 或 UPDATE 或 INSERT 操作的时候,塞进去的每一次执行是一个事务

解决方案

很明显,因为上面 updateTranSyncManager 没有对 List 进行判空处理,导致不一致现象发生,修改完代码如下

java 复制代码
private void updateTranSyncManager(List<TransactionStatus> transactionStatusList) {
    if (!transactionStatusList.isEmpty()) {
        // err: No value for key [DynamicRoutingDataSource] bound to thread
        TransactionSynchronizationManager.bindResource(dynamicRoutingDataSource, dynamicRoutingDataSource);
        // err: Transaction synchronization is not active
        TransactionSynchronizationManager.initSynchronization();
    }
}
相关推荐
说实话起个名字真难啊4 小时前
2026数字中国创新大赛数字安全赛道writeup之web题目一
java·前端·安全
后端AI实验室4 小时前
我用AI把一个外包需求从30天压到5天交付,然后客户说:下次还找你
java·ai
biubiubiu07064 小时前
ChatModel 与 ChatClient 关系完整指南
java
0xDevNull4 小时前
Java 深度解析:for 循环 vs Stream.forEach 及性能优化指南
java·开发语言·性能优化
博风5 小时前
在tomcat应用里添加了一个线程池对象,向这个线程池发送任务,让其执行。 我希望在tomcat停机时,能等待线程池里的任务执行完了再停机,要如何实现?
java·tomcat
studyForMokey5 小时前
【Android面试】Java专题 todo
android·java·面试
一只大袋鼠5 小时前
MyBatis 特性(三):缓存、延迟加载、注解开发
java·数据库·笔记·sql·缓存·mybatis
老毛肚5 小时前
Redis高级
java·数据库·redis
小Y._5 小时前
AQS同步器核心原理深度剖析
java·源码分析·juc·aqs
南棱笑笑生6 小时前
20260420给万象奥科的开发板HD-RK3576-PI适配瑞芯微原厂的Buildroot时使用ll命令
java·大数据·elasticsearch·rockchip