关于多数据源下Spring声明式事务管理失效问题的分析与解决

背景

项目中数据持久层采用了mybatis组件,存在多个数据源,通过@MapperScan的方式声明不同包路径下的dao使用哪个数据源,代码示例如下:

java 复制代码
@Configuration
@MapperScan(basePackages = {"com.company.first.**.dao"}, sqlSessionFactoryRef = "firstSqlSessionFactory")
public class FirstDataSourceConfig {
    @Autowired
    private FirstDataSourceProperties dataSourceProperties;

    @Bean(name = "firstDataSource")
    @Override
    public DataSource dataSource() {
        HikariConfig config = getConfig(dataSourceProperties);
        HikariDataSource dataSource = new HikariDataSource(config);
        return dataSource;
    }

    @Bean(name = "firstTransactionManager")
    @Primary
    @Override
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
    
    @Bean(name = "firstSqlSessionFactory")
    @Primary
    @Override
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        return createSqlSessionFactory(dataSource());
    }
}

其他 SecondDataSourceConfig 代码类似,只是没有 @Primary 注解。

现在有两个服务类A和B,分别有方法1和方法2,方法1调用方法2,方法1的的数据源为1,方法2的数据源为2,示例如下:

java 复制代码
@Service
public class A {
    @Transactional(rollbakFor = Exception.class)
    public void method1() {
        //通过数据源1 操作数据
        dao1.insert(record1);
        
        b.method2();
        
        //其他操作发生错误
        throw new RuntimeException();
    }
}
@Service
public class B {
    @Transactional(rollbakFor = Exception.class)
    public void method2() {
        //通过数据源2 操作数据
        dao2.insert(record2);
    }
}

问题描述:在调用a.method1()时发生了错误,此时数据库中数据1发生了回滚,数据2未发生回滚,这是不合理的,预期的结果是 数据1和2同时回滚。

分析解决

方式一(失败的)

可以看到 方法1和2都通过Spring的注解式声明开启了事务,且这个时候数据源1的操作是有事务管理,数据源2的操作没有事务管理,于是想是不是事务管理器的问题? 这里两个注解声明都没有指定事务管理器,因此这里默认的事务管理器均为 firstTransactionManager。

Spring在没有指定事务管理器的时候会优先找有没有被标注为@Primary的事务管理器,如果没有会找名称为transactionManager的事务管理器,如果都找不到,Spring将无法确定要使用的事务管理器,从而抛出异常。

而方法2的数据源2其实是不被 firstTransactionManager管理的,因此这里方法2是没有事务管理的,于是想通过指定方法2的事务管理器是不是就解决问题了。

java 复制代码
    @Transactional(rollbakFor = Exception.class, transactionManager = "secondTransactionManager")
    public void method2() {
        //通过数据源2 操作数据
        dao2.insert(record2);
    }

经过测试,问题仍然存在。即使通过设置事务传播行为,开启新的事务,问题仍然存在。

java 复制代码
    @Transactional(rollbakFor = Exception.class, transactionManager = "secondTransactionManager" propagation = Propagation.REQUIRES_NEW)
    public void method2() {
        //通过数据源2 操作数据
        dao2.insert(record2);
    }

其实这里因为事务管理器不同,即使方法2的事务传播行为是默认的 Propagation.REQUIRED加入现有事务的类型,因为不同管理器管理的事务是相互独立的,无法直接加入,所以方法2也是在对应的事务管理器下开启新的事务。那么这里为什么方法2的数据2没有跟方法1中的数据1一起回滚?

因为这里方法2是独立的事务2,在方法2调用结束后即结束了事务(提交或回滚),这里方法2本身没有异常,所以事务2提交成功。而事务1在方法2之后发生异常,事务1回滚。其流程如下:

复制代码
方法1调用开始
事务1开启
操作数据1
方法2调用开始
事务2开启
操作数据2
事务2提交
方法2调用结束
其他操作,发生异常
事务1回滚数据1

方式二

通过方式一的分析我们知道需要事务1和事务2一起提交或回滚。那么事务1和事务2能否合并在一个事务内,也就是说方法1和方法2使用同一个事务管理器。

链式事务管理器

java 复制代码
    @Bean(name = "chainedTransactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("firstTransactionManager") DataSourceTransactionManager ds1,
            @Qualifier("secondTransactionManager") DataSourceTransactionManager ds2) {
        return new ChainedTransactionManager(ds1, ds2);
    }

指定事务管理器

java 复制代码
@Service
public class A {
    @Transactional(rollbakFor = Exception.class, transactionManager = "chainedTransactionManager")
    public void method1() {
        //通过数据源1 操作数据
        dao1.insert(record1);
        
        b.method2();
        
        //其他操作发生错误
        throw new RuntimeException();
    }
}

@Service
public class B {
    @Transactional(rollbakFor = Exception.class)
    public void method2() {
        //通过数据源2 操作数据
        dao2.insert(record2);
    }
}

可以看到方法2未指定事务管理器,因为方法2会继承方法1中chainedTransactionManager管理的事务上下文,方法2中操作都会被纳入同一个chainedTransactionManager事务管理。 不过此时因为事务管理器不同,当我指定事务传播行为是 Propagation.NOT_SUPPORTED不起作用,需要指定使用同一个事务管理器。

但是指定 Propagation.NEVER时却会报存在事务的错误,有点奇怪。

其他方案

1 使用JTA/XA分布式事务管理器(推荐用于强一致性场景)

2 使用Seata等分布式事务框架(微服务架构推荐)

3 使用编程式事务管理(精细控制)

4 使用Spring Data的AbstractRoutingDataSource 来管理多个数据源(适合读写分离等场景)

相关推荐
之歆3 小时前
Spring AI入门到实战到原理源码-MCP
java·人工智能·spring
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue医院设备管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
Mr__Miss3 小时前
保持redis和数据库一致性(双写一致性)
数据库·redis·spring
daidaidaiyu4 小时前
Spring IOC 源码学习 一文学习完整的加载流程
java·spring
Knight_AL4 小时前
Spring 事务传播行为 + 事务失效原因 + 传播行为为什么不用其他模式
数据库·sql·spring
五阿哥永琪4 小时前
Spring中的定时任务怎么用?
java·后端·spring
倔强的石头_4 小时前
时序数据时代的“存储与分析困局”解析及金仓解决方案
数据库
计算机毕设VX:Fegn08955 小时前
计算机毕业设计|基于springboot + vue小型房屋租赁系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
TaiKuLaHa5 小时前
Spring Bean的生命周期
java·后端·spring
倔强的石头_5 小时前
场景化落地指南——金仓时序数据库在关键行业的应用实践
数据库