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

相关推荐
9523614 分钟前
Sentinel
java·后端·spring·sentinel·springcloud
今天AI了吗18 分钟前
MCP 协议打通 AI 与国产数据库,SQL 调优全流程一站式闭环
数据库·人工智能·sql
灯澜忆梦32 分钟前
【MySQL11】进阶篇 | 索引_#3使用规则
数据库·sql·mysql·性能优化
名字还没想好☜36 分钟前
Go 的 sync.Cond 实战:用条件变量做等待/通知,比忙轮询省 CPU
开发语言·数据库·后端·golang·go
wWYy.1 小时前
Mysql:有哪些锁?
数据库·mysql
李可以量化1 小时前
Tornado 从了解到精通(一)下:异步与非阻塞 IO 核心原理
java·数据库·tornado
VALENIAN瓦伦尼安教学设备2 小时前
ASHOOTER激光对中仪如何通过颜色确定调整是否合适
数据库·嵌入式硬件·算法
Wang's Blog2 小时前
AI Agent白手起家58: 项目可观测性——用 LangSmith 实现全链路追踪
数据库·人工智能
SL_staff3 小时前
JVS数字底座实践:如何复用企业文档能力快速构建知识类应用
java·数据库·程序员
qq_185198693 小时前
SpingBoot3入门学习-Web开发-三
java·数据库·学习