关于多数据源下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 来管理多个数据源(适合读写分离等场景)

相关推荐
有想法的py工程师4 分钟前
PostgreSQL 查询库中所有表占用磁盘大小、表大小
数据库·postgresql
R_AirMan1 小时前
深入浅出Redis:一文掌握Redis底层数据结构与实现原理
java·数据结构·数据库·redis
Hello.Reader2 小时前
RedisJSON 内存占用剖析与调优
数据库·redis·缓存
wkj0012 小时前
php 如何通过mysqli操作数据库?
android·数据库·php
九皇叔叔4 小时前
【9】PostgreSQL 之 vacuum 死元组清理
数据库·postgresql
风雅的远行者5 小时前
mysql互为主从失效,重新同步
数据库·mysql
宇钶宇夕6 小时前
S7-1200 系列 PLC 中 SCL 语言的 PEEK 和 POKE 指令使用详解
运维·服务器·数据库·程序人生·自动化
绿蚁新亭6 小时前
Spring的事务控制——学习历程
数据库·学习·spring
nbsaas-boot7 小时前
多租户架构下的多线程处理实践指南
java·开发语言·spring
scilwb7 小时前
占用栅格地图数据集
数据库