场景与方案
当一个服务,连接两个数据库,一次接口请求需要向两个数据库同时写入数据,我们使用 @Transactional 注解方式加入事务,此时A数据库写入成功,B数据库写入失败,这时可能数据库A写入的数据不能回滚,原因是 @Transactional 注解中 transactionManager 的参数决定的,只能指定一个事务管理。
A数据库配置代码
java
@Configuration
@MapperScan(basePackages = "com.p.test.testadb.mapper",
annotationClass = Repository.class,
sqlSessionFactoryRef = DataSourceConfig.SQL_SESSION_FACTORY_NAME)
public class DataSourceConfig {
static final String SQL_SESSION_FACTORY_NAME = "aSessionFactory";
public static final String A_TX_MANAGER = "aTxManager";
@Bean(name = "aDataSource")
@ConfigurationProperties(prefix = "spring.datasource")
@Primary
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = A_TX_MANAGER)
@Primary
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean(name = SQL_SESSION_FACTORY_NAME)
@Primary
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper-testadb/*.xml"));
sqlSessionFactoryBean.setDataSource(dataSource());
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setCacheEnabled(false);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
}
B数据库配置代码
java
@Configuration
@MapperScan(basePackages = "com.p.test.testbdb.mapper",
annotationClass = Repository.class,
sqlSessionFactoryRef = DataSourceConfig.SQL_SESSION_FACTORY_NAME)
public class DataSourceConfig {
static final String SQL_SESSION_FACTORY_NAME = "bSessionFactory";
public static final String B_TX_MANAGER = "bTxManager";
@Bean(name = "bDataSource")
@ConfigurationProperties(prefix = "spring.datasource")
@Primary
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = B_TX_MANAGER)
@Primary
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean(name = SQL_SESSION_FACTORY_NAME)
@Primary
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper-testbdb/*.xml"));
sqlSessionFactoryBean.setDataSource(dataSource());
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
configuration.setCacheEnabled(false);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
}
解决方案
解决方案很简单,我们知道 @Transactional 方式加入事务底层其实是AOP实现的,所以我们可以写两个方法,在两个方法上加 @Transactional 事务注解,分别操作数据库,下面上代码
typescript
@Component
public class ABService{
@Autowired
private AService aService;
@Autowired
private BService bService;
public void add(){
aService.a();
bService.b();
}
}
@Component
public class AService{
@Transactional(transactionManager = "aTxManager")
public void a(){
//写入数据到数据库A
}
}
@Component
public class BService {
@Transactional(transactionManager = "bTxManager")
public void b() {
//写入数据到数据库B
}
}
上面这种方式有点low,让他高大上一点,AOP+自定义注解,下面先定义一个多事务注解
java
/**
* @Author: pp
* @DateTime: 2022/3/29 14:33
* @Description: 多事务注解
*/
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransactional {
String[] value() default {};
}
下面是AOP相关代码,通过AOP技术拦截所有使用了 @MultiTransactional 注解的方法
java
/**
* @Author: pp
* @DateTime: 2022/3/29 14:36
* @Description: TODO
*/
@Aspect
@Component
public class MultiTransactionAop {
private final ComboTransaction comboTransaction;
@Autowired
public MultiTransactionAop(ComboTransaction comboTransaction) {
this.comboTransaction = comboTransaction;
}
@Pointcut("@annotation(com.modian.chain.config.datasource.annotation.MultiTransactional)")
public void pointCut() {
}
@Around("pointCut() && @annotation(multiTransactional)")
public Object inMultiTransaction(ProceedingJoinPoint pjp, MultiTransactional multiTransactional) {
return comboTransaction.inCombinedTx(() -> {
try {
return pjp.proceed();
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw new RuntimeException(throwable);
}
}, multiTransactional.value());
}
}
下面实现一个公共事务方法,这代码很简单,将注解中的参数带过来,循环参数中的事务,判断是哪个事务就执行哪个事务对应的的方法
php
/**
* @Author: pp
* @DateTime: 2022/3/29 14:38
* @Description: TODO
*/
@Component
public class ComboTransaction {
@Autowired
private ATxBroker aTxBroker;
@Autowired
private BTxBroker bTxBroker;
public <V> V inCombinedTx(Callable<V> callable, String[] transactions) {
if (callable == null) {
return null;
}
Callable<V> combined = Stream
.of(transactions)
.filter(ele -> !StringUtils.isEmpty(ele))
.distinct()
.reduce(callable, (r, tx) -> {
switch (tx) {
case DataSourceBusinessConfig.A_TX_MANAGER:
return () -> aTxBroker.inTransaction(r);
case DataSourceChainConfig.B_TX_MANAGER:
return () -> aTxBroker.inTransaction(r);
default:
return null;
}
}, (r1, r2) -> r2);
try {
return combined.call();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
ATxBroker和BTxBroker的实现,下面这个代码应该很眼熟吧,和上面我说的很low的实现思想是一样的,只不过这里我们统一封装了而已
php
/**
* @Author: pp
* @DateTime: 2022/3/29 14:39
* @Description: TODO
*/
@Component
public class ATxBroker {
@Transactional(DataSourceBusinessConfig.A_TX_MANAGER)
public <V> V inTransaction(Callable<V> callable){
try {
return callable.call();
}catch (RuntimeException e){
throw e;
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
/**
* @Author: pp
* @DateTime: 2022/3/29 14:39
* @Description: TODO
*/
@Component
public class BTxBroker {
@Transactional(DataSourceBusinessConfig.B_TX_MANAGER)
public <V> V inTransaction(Callable<V> callable){
try {
return callable.call();
}catch (RuntimeException e){
throw e;
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
使用方式如下
csharp
@Component
public class ABService{
@MultiTransactional(value = {DataSourceBusinessConfig.A_TX_MANAGER, DataSourceChainConfig.B_TX_MANAGER})
public void add(){
//A库写入数据
a.add();
//B库写入数据
b.add();
}
}
就此优雅的解决了这个问题,从技术上来说没有任何实现难度,我们只需要知道 @Transactional 注解的使用方式,知道AOP原理即可,主要还是设计思想。