[[380215]] Spring Boot プロジェクトでは、複数のデータ ソースに接続するのが非常に一般的です。 複数のデータ ソースを使用する場合、データ ソース A とデータ ソース B への更新をトランザクション的に行うという特別なシナリオも発生します。このような例は非常に一般的です。たとえば、注文データベースに注文レコードを作成し、製品データベースの製品在庫を減算するなどです。在庫減算が失敗した場合は、注文の作成もロールバックする必要があります。 これら 2 つのデータが同じデータベース内にある場合は、先に紹介したトランザクション管理を通じて簡単に解決できます。ただし、2 つの操作が異なるデータベースで行われる場合、これは不可能です。 この記事では、この問題の解決策である JTA トランザクションを紹介します。 JTAとは JTA、正式名称: Java Transaction API。 JTA トランザクションは、JDBC トランザクションよりも強力です。 JTA トランザクションには複数の参加者が存在する可能性がありますが、JDBC トランザクションは単一のデータベース接続に限定されます。したがって、複数のデータベースを同時に操作する場合、JTA トランザクションを使用すると、JDBC トランザクションの欠点を補うことができます。 Spring Boot 2.x では、次の 2 つの JTA 実装が統合されています。 Atomikos: spring-boot-starter-jta-atomikos 依存関係を導入することで使用可能 Bitronix: spring-boot-starter-jta-bitronix依存関係を導入することで使用可能 Bitronix は Spring Boot 2.3.0 以降では非推奨となっているため、以下のハンズオンセッションでは Atomikos を例に JTA の使い方を紹介します。 自分で試してみましょう Spring Boot で JTA を使用して、複数のデータ ソースでトランザクション管理を実装する方法を見てみましょう。 準備 - ここでは、最も基本的な JdbcTemplate を使用してデータ アクセスを実装します。そのため、JdbcTemplate を使用して複数のデータ ソースを構成する方法がわからない場合は、まず JdbcTemplate の複数のデータ ソース構成を確認することをお勧めします。
シーン設定: - test1とtest2という2つのライブラリがあるとします。
- 両方のデータベースにUserテーブルがあり、これら2つのテーブルのデータが一致していることを期待しています。
- 両方のテーブルにすでにレコードが存在するものとします: name=aaa, age=30; 2 つのテーブルのデータは一致しているため、更新が必要な場合は、両方のデータベースの User テーブルを更新する必要があり、両方とも成功するか、両方とも失敗します。
操作の詳細 pom.xml に JTA 実装 Atomikos Starter を追加します。 - <依存関係>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-jta-atomikos</artifactId>
- </依存関係>
application.properties 構成ファイルで 2 つの test1 および test2 データ ソースを構成します。 - spring.jta.enabled =真
-
- spring.jta.atomikos.datasource.primary .xa -properties.url=jdbc:mysql://localhost:3306/test1
- spring.jta.atomikos.datasource.primary.xa -プロパティ.user =ルート
- spring.jta.atomikos.datasource.primary.xa -プロパティ.パスワード= 12345678
- spring.jta.atomikos.datasource.primary .xa -データソースクラス名=com.mysql.cj.jdbc.MysqlXADataSource
- spring.jta.atomikos.datasource.primary.unique -リソース-名前= test1
- spring.jta.atomikos.datasource.primary.max -プール-サイズ= 25
- spring.jta.atomikos.datasource.primary.min -プール-サイズ= 3
- spring.jta.atomikos.datasource.primary.max -有効期間=20000
- spring.jta.atomikos.datasource.primary.borrow-接続-タイムアウト=10000
-
- spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/test2
- spring.jta.atomikos.datasource.secondary.xa -properties.user =root
- spring.jta.atomikos.datasource.secondary.xa- properties.password =12345678
- spring.jta.atomikos.datasource.secondary.xa-データソースクラス名=com.mysql.cj.jdbc.MysqlXADataSource
- spring.jta.atomikos.datasource.secondary です。一意のリソース名=test2
- spring.jta.atomikos.datasource.secondary です。最大プールサイズ= 25
- spring.jta.atomikos.datasource.secondary です。最小プールサイズ=3
- spring.jta.atomikos.datasource.secondary.max -有効期間 = 20000
- spring.jta.atomikos.datasource.secondary.borrow-接続-timeout=10000
マルチデータソース構成クラスを作成する - @構成
- パブリッククラス DataSourceConfiguration {
-
- @主要な
- @ビーン
- @ConfigurationProperties(プレフィックス = "spring.jta.atomikos.datasource.primary" )
- パブリックデータソースプライマリデータソース() {
- 新しい AtomikosDataSourceBean()を返します。
- }
-
- @ビーン
- @ConfigurationProperties(プレフィックス = "spring.jta.atomikos.datasource.secondary" )
- パブリックデータソースセカンダリデータソース() {
- 新しい AtomikosDataSourceBean()を返します。
- }
-
- @ビーン
- パブリックJdbcTemplate primaryJdbcTemplate(@Qualifier( "primaryDataSource" ) データソース primaryDataSource) {
- 新しい JdbcTemplate(primaryDataSource)を返します。
- }
-
- @ビーン
- パブリックJdbcTemplate secondaryJdbcTemplate(@Qualifier( "secondaryDataSource" ) データソース secondaryDataSource) {
- 新しい JdbcTemplate(secondaryDataSource)を返します。
- }
-
- }
ここでの異なる構成に加えて、DataSource は AtomikosDataSourceBean も使用することに注意してください。複数のデータ ソースの以前の構成で使用された構成クラスと実装クラスの違いに注意してください。 2 つの異なる状況をシミュレートするサービス実装を作成します。 - @サービス
- パブリッククラスTestService{
-
- プライベート JdbcTemplate primaryJdbcTemplate;
- プライベート JdbcTemplate セカンダリ JdbcTemplate;
-
- パブリックTestService(JdbcTemplate プライマリ JdbcTemplate、JdbcTemplate セカンダリ JdbcTemplate) {
- プライマリJdbcテンプレートを作成します。
- this.secondaryJdbcTemplate = セカンダリJdbcTemplate;
- }
-
- @トランザクション
- パブリックvoidtx() {
- // test1ライブラリのデータを変更する
- プライマリJdbcテンプレート。更新( "更新ユーザー set age = ? where name = ?" 、 30、 "aaa" );
- // test2ライブラリのデータを変更する
- セカンダリJdbcテンプレート。更新( "更新ユーザー set age = ? where name = ?" 、 30、 "aaa" );
- }
-
- @トランザクション
- パブリックボイドtx2() {
- // test1ライブラリのデータを変更する
- プライマリJdbcテンプレート。更新( "更新ユーザー set age = ? where name = ?" 、 40、 "aaa" );
- // シミュレーション: test2 ライブラリを変更する前に例外をスローします
- 新しい RuntimeException() をスローします。
- }
-
- }
ここで、tx 関数は 2 つのステートメントの更新操作であり、通常は成功します。 tx2 関数では、例外を人為的に作成します。この例外は、test1 データベースのデータが更新された後に生成されます。このようにして、test1 の更新が成功したかどうか、また JTA を使用してロールバックできるかどうかをテストできます。 テストクラスを作成し、テストケースを書く - @SpringBootTest(クラス = Chapter312Application.class)
- パブリッククラス Chapter312ApplicationTests {
-
- オートワイヤード
- 保護された Jdbc テンプレート primaryJdbcTemplate;
- オートワイヤード
- 保護された JdbcTemplate セカンダリ JdbcTemplate;
-
- オートワイヤード
- プライベート TestService testService;
-
- @テスト
- パブリックvoid test1()は例外をスローします{
- // 正しい更新状況
- テストサービス.tx();
- Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject( "select age from user where name=?" , Integer .class, "aaa" ));
- Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject( "select age from user where name=?" , Integer .class, "aaa" ));
- }
-
- @テスト
- パブリックvoid test2() は例外をスローします {
- // 更新に失敗しました
- 試す {
- テストサービス.tx2();
- } キャッチ (例外 e) {
- e.printStackTrace();
- ついに
- // 部分的な更新に失敗しました。test1 の更新をロールバックする必要があります
- Assertions.assertEquals(30, primaryJdbcTemplate.queryForObject( "select age from user where name=?" , Integer .class, "aaa" ));
- Assertions.assertEquals(30, secondaryJdbcTemplate.queryForObject( "select age from user where name=?" , Integer .class, "aaa" ));
- }
- }
-
- }
ここに 2 つのテストケースがあります: - テスト 1: 意図的な例外がないため、2 つのデータベースの更新は期待どおりに成功します。したがって、2 つのデータは name=aaa に従ってチェックされ、年齢が 30 に更新されているかどうかが確認されます。
- test2: tx2 関数は、test1 の name=aaa のユーザーの年齢を 40 に更新し、例外をスローします。 JTA トランザクションが有効になると、経過時間は 30 にロールバックされます。したがって、ここでのチェックは、2 つのデータベース内の aaa ユーザーの経過時間が 30 である必要があるということです。つまり、JTA トランザクションが有効になり、test1 および test2 データベース内のユーザー テーブル データがダーティ データを作成せずに一貫して更新されることが保証されます。
テスト検証 上記の単体テストを実行します。 起動フェーズ中のログを見ると、次のような Atomikos 初期化ログ出力が確認できます。 - 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.145 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.146 INFO 8868
- 2021-02-02 19:00:36.147 INFO 8868
- 2021-02-02 19:00:36.184 INFO 8868
- 2021-02-02 19:00:36.203 INFO 8868
同時に、トランザクションに関するログ情報は、transaction-logs ディレクトリでも見つかります。 - { "id" : "127.0.0.1.tm161226409083100001" 、 "wasCommitted" : true 、 "participants" :[{ "uri" : "127.0.0.1.tm1" 、 "state" : "COMMITTING" 、 "expires" :1612264100801、 "resourceName" : "test1" },{ "uri" : "127.0.0.1.tm2" 、 "state" : "COMMITTING" 、 "expires" :1612264100801、 "resourceName" : "test2" }]}
- { "id" : "127.0.0.1.tm161226409083100001" 、 "wasCommitted" : true 、 "participants" :[{ "uri" : "127.0.0.1.tm1" 、 "state" : "TERMINATED" 、 "expires" :1612264100804、 "resourceName" : "test1" },{ "uri" : "127.0.0.1.tm2" 、 "state" : "TERMINATED" 、 "expires" :1612264100804、 "resourceName" : "test2" }]}
- { "id" : "127.0.0.1.tm161226409092800002" 、 "wasCommitted" : false 、 "participants" :[{ "uri" : "127.0.0.1.tm3" 、 "state" : "TERMINATED" 、 "expires" :1612264100832、 "resourceName" : "test1" }]}
コードサンプル この記事に関連する例については、次のリポジトリの chapter3-12 ディレクトリを参照してください。 Github: https://github.com/dyc87112/SpringBoot-Learning/ gitee: https://gitee.com/didispace/SpringBoot-Learning/ |