はじめに
問題の紹介
時限タスクのため、同じメソッド内で複数のデータソースを操作する必要がありますが、データソースが異なるため、トランザクションを使用するとエラーが報告されます。
public int addWithTransation() {
 int currentTimeMills = (int) Instant.now().getEpochSecond();
 CreditRecord creditRecordA = new CreditRecord();
 creditRecordA.setId(3L);
 creditRecordA.setBeforeAmount(100);
 creditRecordA.setChangeAmount(50);
 creditRecordA.setAfterAmount(150);
 creditRecordA.setCreateTime(currentTimeMills);
 creditRecordA.setUpdateTime(currentTimeMills);
 CreditRecord creditRecordB = new CreditRecord();
 creditRecordB.setId(3L);
 creditRecordB.setBeforeAmount(0);
 creditRecordB.setChangeAmount(-50);
 creditRecordB.setAfterAmount(-50);
 creditRecordB.setCreateTime(currentTimeMills);
 creditRecordB.setUpdateTime(currentTimeMills);
 
 this.baseMapper.insert(creditRecordA);
 
 
 creditRecordDB2Service.add(creditRecordB);
 if(true){
 throw new RuntimeException("例外を投げる");
 }
 return 1;
 }
その原理を知るために、後でソースコードを見てみました。トランザクションを追加しても、データソースは最初のデータソースのままです。
また、DataSourceはプロキシ時にDataSource.DataSource()を設定することで初期化されます。
さて、トランザクション使用時にデータベース接続時に設定したデータソースを変更できない問題を解決するためには、指定したマッパーにデータソースを指定するように設定します。指定したマッパーにデータソースを渡すように設定することで、この問題を解決することができます。
@Configuration
@MapperScan(basePackages = "com.example.multisource.dao.db1", sqlSessionFactoryRef = "db1SqlSessionFactory")
//指定したマッパーを特定のsqlsessionFactoryに割り当てることで、トランザクション中にデータソースが変更されない問題を解決する。
public class Db1Config {
 @Bean(name = "db1")
 @ConfigurationProperties(prefix = "spring.datasource.druid.db1" )
 public DataSource db1() {
 return DruidDataSourceBuilder.create().build();
 }
 // トランザクション・コントローラー
 @Bean(name = "db1TransactionManager")
 @Primary
 public DataSourceTransactionManager dp1TransactionManager() {
 return new DataSourceTransactionManager(db1());
 }
 @Bean(name = "db1SqlSessionFactory")
 @Primary
 public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1") DataSource db1)
 throws Exception {
 final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
 sessionFactory.setDataSource(db1);
 sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
 .getResources("classpath:/mapper/db1/*.xml"));
 
 /*sqlコンソールをセットアップして印刷する*/
 com.baomidou.mybatisplus.core.MybatisConfiguration configuration = new com.baomidou.mybatisplus.core.MybatisConfiguration();
 configuration.setLogImpl(StdOutImpl.class);
 sessionFactory.setConfiguration(configuration);
 
 return sessionFactory.getObject();
 }
}
@Configuration
@MapperScan(basePackages = "com.example.multisource.dao.db2", sqlSessionFactoryRef = "db2SqlSessionFactory")
public class Db2Config {
 @Bean(name = "db2")
 @ConfigurationProperties(prefix = "spring.datasource.druid.db2" )
 public DataSource db2() {
 return DruidDataSourceBuilder.create().build();
 }
 
 
 @Bean(name = "db2TransactionManager")
 @Primary
 public DataSourceTransactionManager db2TransactionManager() {
 return new DataSourceTransactionManager(db2());
 }
 @Bean(name = "db2SqlSessionFactory")
 @Primary
 public SqlSessionFactory db2SqlSessionFactory(@Qualifier("db2") DataSource db2)
 throws Exception {
 final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
 sessionFactory.setDataSource(db2);
 sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
 .getResources("classpath:/mapper/db2/*.xml"));
 
 /*sqlコンソールをセットアップして印刷する*/
 com.baomidou.mybatisplus.core.MybatisConfiguration configuration = new com.baomidou.mybatisplus.core.MybatisConfiguration();
 configuration.setLogImpl(StdOutImpl.class);
 sessionFactory.setConfiguration(configuration);
 
 return sessionFactory.getObject();
 }
}
エラー報告時のロールバックの解決
インターネット上で多くのデモを見かけましたが、そのほとんどは欠落しているか、話半分にしかなっていません。
私の一般的な考えは、@Transactionアノテーションは複製できないので、2つのトランザクションを開始するために2つのトランザクションマネージャを開始する独自のカスタムアノテーションを書きます。
カスタムアノテーション
/**
 * @ClassName DataSource
 * @Author kris
 * @Date 
 **/
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTm {
}
AOP
@Component
@Slf4j
@Aspect
@Order(-1)
public class TsetAspect {
 @Pointcut("@within(com.example.multisource.annonation.MultiTm) || @annotation(com.example.multisource.annonation.MultiTm)")
 public void TsetAspect(){
 
 }
 @Around(value = "TsetAspect()")
 public Object transactionalGroupAspectArround(ProceedingJoinPoint pjp) throws Throwable{
// db1Manager.setDataSource((DataSource) SpringContextUtil.getBean("db1"));
// db2Manager.setDataSource((DataSource) SpringContextUtil.getBean("db2"));
 DataSourceTransactionManager db1Manager = (DataSourceTransactionManager) SpringContextUtil
 .getBean("db1TransactionManager");
 TransactionStatus transactionDB1Status = db1Manager
 .getTransaction(new DefaultTransactionDefinition());
 DataSourceTransactionManager db2Manager = (DataSourceTransactionManager) SpringContextUtil
 .getBean("db2TransactionManager");
 TransactionStatus transactionDB2Status = db2Manager
 .getTransaction(new DefaultTransactionDefinition());
 
 
 try{
 Object obj = pjp.proceed();
 db2Manager.commit(transactionDB2Status);
//この書き方をしないと、トランザクションがアクティブにならない。db1Managerが最初に起動し、db2Managerが再び起動するため、db2Managerは実際にはdb1Managerにラップされている。そのため、コミットやロールバックをするときは、db2Managerを最初にコミットまたはロールバックする必要がある。これはLIFOの原則に沿っており、Stackによって最適化できる。
 db1Manager.commit(transactionDB1Status);
 return obj;
 }catch(Exception e){
 log.info(e.getMessage());
 db2Manager.rollback(transactionDB2Status);
 db1Manager.rollback(transactionDB1Status);
 return null;
 } }
}
上記に基づいて最適化を行います:
@Component
@Slf4j
@Aspect
@Order(-1)
public class TsetAspect {
 @Pointcut("@within(com.example.multisource.annonation.MultiTm) || @annotation(com.example.multisource.annonation.MultiTm)")
 public void TsetAspect(){
 
 }
 @Around(value = "TsetAspect() && @annotation(multiTm)")
 public Object transactionalGroupAspectArround(ProceedingJoinPoint pjp, MultiTm multiTm) throws Throwable{
 Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<>();
 Stack<TransactionStatus> transactionStatusStack = new Stack<>();
 if (multiTm.transactionManagers().length<1){
 log.info("[トランザクションのオープンに失敗した]不特定多数データソースマネージャー");
 return null;
 }
 for(String transationMangaeName: multiTm.transactionManagers()){
 DataSourceTransactionManager dbManager = (DataSourceTransactionManager) SpringContextUtil.getBean(transationMangaeName);
 TransactionStatus transactionDBStatus = dbManager.getTransaction(new DefaultTransactionDefinition());
 dataSourceTransactionManagerStack.push(dbManager);
 transactionStatusStack.push(transactionDBStatus);
 }
 
 try{
 Object obj = pjp.proceed();
 while(!dataSourceTransactionManagerStack.isEmpty()){
 dataSourceTransactionManagerStack.pop().commit(transactionStatusStack.pop());
 }
 return obj;
 }catch(Exception e){
 log.info(e.getMessage());
 while(!dataSourceTransactionManagerStack.isEmpty()){
 dataSourceTransactionManagerStack.pop().rollback(transactionStatusStack.pop());
 }
 return null;
 }
 }
}
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTm {
 String[] transactionManagers() default {};
}
 @Override
 @MultiTm(transactionManagers={"db1TransactionManager","db2TransactionManager"})
 public int addWithTransation() {
 int currentTimeMills = (int) Instant.now().getEpochSecond();
 CreditRecord creditRecordA = new CreditRecord();
 creditRecordA.setId(3L);
 creditRecordA.setBeforeAmount(100);
 creditRecordA.setChangeAmount(50);
 creditRecordA.setAfterAmount(150);
 creditRecordA.setCreateTime(currentTimeMills);
 creditRecordA.setUpdateTime(currentTimeMills);
 CreditRecord creditRecordB = new CreditRecord();
 creditRecordB.setId(3L);
 creditRecordB.setBeforeAmount(0);
 creditRecordB.setChangeAmount(-50);
 creditRecordB.setAfterAmount(-50);
 creditRecordB.setCreateTime(currentTimeMills);
 creditRecordB.setUpdateTime(currentTimeMills);
 
 this.baseMapper.insert(creditRecordA);
 
 
 creditRecordDB2Service.add(creditRecordB);
 if(true){
 throw new RuntimeException("例外を投げる");
 }
 return 1;
 }
操作方法
 @MultiTm
// @Transactional("db1TransactionManager")
// @Transactional("db2TransactionManager")
 public int addWithTransation() {
 int currentTimeMills = (int) Instant.now().getEpochSecond();
 CreditRecord creditRecordA = new CreditRecord();
 creditRecordA.setId(3L);
 creditRecordA.setBeforeAmount(100);
 creditRecordA.setChangeAmount(50);
 creditRecordA.setAfterAmount(150);
 creditRecordA.setCreateTime(currentTimeMills);
 creditRecordA.setUpdateTime(currentTimeMills);
 CreditRecord creditRecordB = new CreditRecord();
 creditRecordB.setId(3L);
 creditRecordB.setBeforeAmount(0);
 creditRecordB.setChangeAmount(-50);
 creditRecordB.setAfterAmount(-50);
 creditRecordB.setCreateTime(currentTimeMills);
 creditRecordB.setUpdateTime(currentTimeMills);
 
 this.baseMapper.insert(creditRecordA);
 
 
 creditRecordDB2Service.add(creditRecordB);
 if(true){
 throw new RuntimeException("例外を投げる");
 }
 return 1;
 }
1
2





