blog

Transation - 複数のデータソースで複数のトランザクションを1つのメソッドで解決する

はじめに\n問題紹介\n時限タスクなので、複数のデータソースを同じメソッドで操作する必要がありますが、データソースが異なるため、トランザクションを使用するとエラーが報告されることがわかります。\nパブ...

Dec 26, 2020 · 8 min. read
シェア

はじめに

問題の紹介

時限タスクのため、同じメソッド内で複数のデータソースを操作する必要がありますが、データソースが異なるため、トランザクションを使用するとエラーが報告されます。

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;
 }

その原理を知るために、後でソースコードを見てみました。トランザクションを追加しても、データソースは最初のデータソースのままです。

トランザクションのSpringソースコード解析

メソッドに@Transationアノテーションがあるか調べてきてください。
データベースへの接続を取得する場合は、containDataSource().getConnection() を使用してデータベース接続を作成する DataSource を取得します。

また、DataSourceはプロキシ時にDataSource.DataSource()を設定することで初期化されます。

このトランザクションでは、データベースへの接続を取得した時点から 1 つの 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

Read next

要素コンポーネントソースコード研究-入力数値カウンタ

この記事では、要素コンポーネントのソースコードを読み込んで、それに対応する機能を向上させるためのコンポーネントを記述するステップを自動的に行うという研究アイデアを紹介しました。 今回はその続きです。 基本的な準備の後、基本的な実装に入ります。 今回は、Inputコンポーネントを再利用し、さらに足し算・引き算のロジックを実現するために、左右に2つのボタンを追加・削減します。そして、最大値と最小値の制御、ボタンの無効化、基本...

Dec 26, 2020 · 6 min read

JavaのWebクッキー

Dec 25, 2020 · 8 min read

shiro --- 認証について

Dec 25, 2020 · 1 min read

YYCacheを理解する

Dec 25, 2020 · 9 min read