経緯
開発しているアプリケーションで分散トランザクションの実装が必要になりました。
一般的なアプリケーション開発では、とある処理で複数のテーブルに対して処理を行い、その処理内でテーブル間の整合性を担保しなければならない場合、トランザクションを利用し実現すると思います。
対して、アプリケーションから複数のデータベースに対して処理を行い、DB間でその処理の整合性を担保しなければならない場面は、分散トランザクションを利用するなどが候補として挙がります。
今回は、この分散トランザクションについて、実装方法を検討し、実装したので記事にしていきます。
実装の部分のみ読みたい方はこちらからどうぞ。
想定読者
- 分散トランザクションについて知りたい方
- 分散トランザクションの実装例を知りたい方
前提知識
実現方法の調査
実装部分では、Java, SpringBootのアプリケーション開発知識が必要となります。
使用技術
この記事で実装する際に使用している技術です↓
項目 | 技術 |
---|---|
言語 | Java 11 |
フレームワーク | SpringBoot 2.7.2 |
ビルドツール | maven |
DB1 | MySQL 8.0.30 |
DB2 | SQL Server 2012 |
分散トランザクション実現方法の調査
【参考】MyBatis トランザクションについてのドキュメント
- 標準的な設定
- Container Managed Transactions
- トランザクションをプログラム的に制御する
X/Open XA について
システムの標準化を行なっている業界団体 X/Open が策定した分散トランザクション処理のための標準規格のことで、
「X/Open XA」(Extended Architecture)あるいは「X/Open DTPモデル」(Distributed Transaction Processing model)という。
XAは、トランザクションマネージャーとリソースマネージャー間で2相コミット実現するプログラムインタフェースを規定している。主にTXインタフェースおよびXAインタフェースがある。
分散トランザクションに関する参考記事
実現方法の候補
-
疑似的な分散トランザクションを実装する
→ ChainedTransactionManagerを利用することで実現可能
Spring Data Commonsのライブラリなどが利用できる -
分散トランザクションを実装する
→ 分散トランザクションを実現・管理するオープンのソースライブラリを利用することで実現可能
atomikosなどが利用できる
★こちらの方が厳密にトランザクション処理が可能なので、こちらを推奨します
【参考】実現方法
Spring Data Commons
atomikos
【参考記事】疑似的な分散トランザクションの実装
【実装例】1つのトランザクションで複数のDBを更新するアプリケーションでChainedTransactionManagerを利用してみた
【参考記事】分散トランザクションの実装
【概念・実装イメージ】Atomikos Transaction Manager を使用する
【実装例】Atomikosによる分散トランザクション処理を実装してみた(完成イメージ編)
【実装例】★Atomikosによる分散トランザクション処理を実装してみた(ソースコード編)
【実装例】★1つのトランザクションで複数のDB(OracleとSQL Server)を更新するアプリケーションでAtomikosを利用してみた
実際に実装してみる
※無償でも使用可能なatomikosのライブラリを使用します
※↓のソースや手順は、★の記事をかなり参考にしています
依存関係を追加
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-spring-boot-starter</artifactId>
<version>5.0.9</version>
</dependency>
こちらのwebサイトなどでmaven依存関係を検索すると違うものが出てくるようので、注意が必要です。
※微妙に違う依存関係だと一部のjarファイルがないなどの事象が発生しました
例えば以下のものだと、javax.transaction.UserTransaction
がimport
できませんでした。
<artifactId>transactions-jta</artifactId>
データソースの設定
データソースの設定は、接続先となるDBの数だけあると思いますので、
全ての設定クラスに対して分散トランザクションのBean定義datasource()
の追加実装を行います。
※変更がない部分は基本的に何も触っていません
@Configuration
@MapperScan(basePackages = {"jp.co.sample.repository"},
sqlSessionFactoryRef = SampleDataSourceConfig.SQL_SESSION_FACTORY_SAMPLE)
public class SampleDataSourceConfig {
public static final String SQL_SESSION_FACTORY_SAMPLE = "sqlSessionFactorySample";
public static final String DATA_SOURCE_SAMPLE = "datasourceSample";
public static final String TX_MANAGER_SAMPLE = "txManagerSample";
@Autowired
private ApplicationContext context;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.sample")
public DataSourceProperties sampleDataSource() {
return new DataSourceProperties();
}
- @Bean(name = {DATA_SOURCE_SAMPLE})
- public DataSource datasource(@Qualifier("sampleDataSource") DataSourceProperties properties) {
- return properties.initializeDataSourceBuilder().build();
- }
+ @Bean(name = {DATA_SOURCE_SAMPLE})
+ public DataSource datasource(@Qualifier("sampleDataSource") DataSourceProperties properties) {
+ // SQLServerXADataSourceオブジェクトを生成
+ MysqlXADataSource xaDataSource = new MysqlXADataSource();
+ xaDataSource.setURL(properties.getUrl());
+ xaDataSource.setUser(properties.getUsername());
+ xaDataSource.setPassword(properties.getPassword());
+
+ AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
+ // 一意なリソース名・SQLServerXADataSourceオブジェクトを設定し返却
+ atomikosDataSourceBean.setUniqueResourceName(DATA_SOURCE_SAMPLE);
+ atomikosDataSourceBean.setXaDataSource(xaDataSource);
+ atomikosDataSourceBean.setPoolSize(5); // デフォルト1
+ return atomikosDataSourceBean;
+ }
@Bean(name = {SQL_SESSION_FACTORY_SAMPLE})
public SqlSessionFactory sqlSessionFactory(@Qualifier(DATA_SOURCE_SAMPLE) DataSource datasource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(datasource);
// MyBatis-configの設定をセット
sqlSessionFactoryBean.setConfigLocation(context.getResource(
context.getEnvironment().getProperty("mybatis.config.classpath")
));
return sqlSessionFactoryBean.getObject();
}
}
トランザクション処理実装
トランザクション処理イメージ
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.jta.JtaTransactionManager;
@Service
public class SyncService {
@Autowired
private JtaTransactionManager jtaTxManager;
private void main() {
try {
// 分散トランザクション取得
UserTransaction transaction = this.jtaTxManager.getUserTransaction();
// トランザクション開始
transaction.begin();
try{
// DB1への処理
// DB2への処理
// コミット
transaction.commit();
} catch(Exception e) {
}
} catch(Exception e) {
}
}
まとめ
Java/SpringBootで分散トランザクションを利用したい場合は、Atomikosなどが採用可能
関連記事
SQLServerで分散トランザクションを利用するための手順
追記
MysqlXAException (2022/10/12)
RDBにMySQLを採用し、以下の実行時例外が発生した場合の対処方法について記載します。
com.mysql.cj.jdbc.MysqlXAException: XAER_RMERR: Fatal error occurred in the transaction branch - check your data for consistency
こちらの例外について調査すると、mysqlにアクセスしているアカウントに、「XA_RECOVER_ADMIN」のシステム権限がない場合に発生するという情報が得られました。つまり、解決策としては「XA_RECOVER_ADMIN」のシステム権限を付与する、ことになります。
MySQLの8.0より前では、XAトランザクションを開始したユーザー以外のユーザーによるXAトランザクションのコミットやロールバックが発生する可能性があり、MySQL8.0系で対応したため、発生するようになっているみたいです。
以下、MySQLの公式ドキュメントURLと記載事項の抜粋。
「XA_RECOVER_ADMIN」で検索すると見つかります。
MySQL 8.0 より前は、すべてのユーザーが XA RECOVER ステートメントを実行して、未処理の準備済 XA トランザクションの XID 値を検出できたため、XA トランザクションを開始したユーザー以外のユーザーによる XA トランザクションのコミットまたはロールバックが発生する可能性がありました。 MySQL 8.0 では、XA RECOVER は XA_RECOVER_ADMIN 権限を持つユーザーにのみ許可されます。これは、それを必要とする管理ユーザーにのみ付与されることが予想されます。 たとえば、XA アプリケーションがクラッシュし、ロールバックできるようにアプリケーションによって開始された未処理のトランザクションを検索する必要がある場合などです。 この権限要件により、ユーザーは自分以外の未処理の準備済 XA トランザクションの XID 値を検出できなくなります。 XA トランザクションを開始したユーザーが XID を認識しているため、XA トランザクションの通常のコミットまたはロールバックには影響しません。
XA_RECOVER_ADMIN 権限の付与
MySQL ユーザーアカウントに権限を付与することができる「GRANT」のステートメントを使用します。
書式
GRANT priv_type[, priv_type] ON priv_level TO user
指定値
- priv_type[, priv_type]
権限の種類 - priv_level
権限指定のレベル - user
権限付与対象のユーザー
ユーザーは、以下のクエリで確認可能
select user, host from mysql.user;
ここまでの情報を整理し、例えばuserが「admin」、hostが「%」の場合だと、
XA_RECOVER_ADMIN権限の付与は以下となります。
mysql> GRANT XA_RECOVER_ADMIN ON *.* TO 'admin'@'%';
=> Query OK, 0 rows affected (0.01 sec)
「Query OK」と出力されれば成功です。
念のため確認する場合はこちらで確認。
mysql> show grants for admin@%;
私の場合は、これらの対応後、アプリを実行すると例外は発生しなくなりました。