背景
项目中数据持久层采用了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 来管理多个数据源(适合读写分离等场景)