Atomikos を利用した複数データベース間の分散トランザクション実装ガイド

Spring Boot のアプリケーションにおいて、複数の独立したデータベース間で事務の整合性を担保するには、分散トランザクション(XA)のプロトコル対応が必要です。ここでは、Atomikos トランザクションマネージャを採用し、MyBatis Plus と併用する際の具体的な実装フローについて説明します。

依存ライブラリの導入

まずは、プロジェクトのビルドシステムに必要な Maven パッケージを追加します。以下は、Atomikos のスタターター、ORM ツール、そしてデータソース管理用のドライバを示しています。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<!-- ORM フレームワーク -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3</version>
</dependency>
<!-- コネクションプール -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.18</version>
</dependency>

設定ファイルへの記録

複数のデータベース接続情報を application.yml に定義します。個別のネームスペースを使用して切り分けることが推奨されます。

spring:
  datasource:
    master_db:
      url: jdbc:postgresql://192.168.0.50:5432/core_data?useUnicode=true&characterEncoding=UTF-8
      username: admin_master
      password: SecurePass#Master
    slave_db:
      url: jdbc:postgresql://192.168.0.51:5432/sub_records?useUnicode=true&characterEncoding=UTF-8
      username: admin_slave
      password: SecurePass#Slave

データソースの Java 設定

データソースごとに独立した設定クラスを作成することで、bean の衝突を防ぎ、設定内容を明確化できます。ここでは XA 準拠の DataSource ビーンを作成し、MyBatis の SqlSessionFactory に紐付けます。

メインデータベース設定:

@Configuration
public class PrimaryDataSourceConfig {

    @Value("${spring.datasource.master_db.url}")
    private String url;
    @Value("${spring.datasource.master_db.username}")
    private String username;
    @Value("${spring.datasource.master_db.password}")
    private String password;

    @Bean(name = "primaryXaDataSource")
    public DataSource primaryDataSource() throws Exception {
        AtomikosDataSourceBean atomikos = new AtomikosDataSourceBean();
        DruidXADataSource druid = new DruidXADataSource();
        druid.setUrl(url);
        druid.setUsername(username);
        druid.setPassword(password);
        
        atomikos.setXaDataSource(druid);
        atomikos.setUniqueResourceName("primary-db-resource");
        return atomikos;
    }

    @Bean(name = "primarySessionFactory")
    public SqlSessionFactory primaryFactory(@Qualifier("primaryXaDataSource") DataSource ds) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(ds);
        factory.setTypeAliasesPackage("com.company.core.entity");
        factory.getConfiguration().setMapUnderscoreToCamelCase(true);
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources("classpath*:mapper/master/*.xml"));
        return factory.getObject();
    }

    @Bean(name = "primarySessionTemplate")
    public SqlSessionTemplate template(@Qualifier("primarySessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }
}

サブデータベース設定:

@Configuration
public class SecondaryDataSourceConfig {

    @Value("${spring.datasource.slave_db.url}")
    private String url;
    @Value("${spring.datasource.slave_db.username}")
    private String username;
    @Value("${spring.datasource.slave_db.password}")
    private String password;

    @Bean(name = "secondaryXaDataSource")
    public DataSource secondaryDataSource() throws Exception {
        AtomikosDataSourceBean atomikos = new AtomikosDataSourceBean();
        DruidXADataSource druid = new DruidXADataSource();
        druid.setUrl(url);
        druid.setUsername(username);
        druid.setPassword(password);
        
        atomikos.setXaDataSource(druid);
        atomikos.setUniqueResourceName("secondary-db-resource");
        return atomikos;
    }

    @Bean(name = "secondarySessionFactory")
    public SqlSessionFactory secondaryFactory(@Qualifier("secondaryXaDataSource") DataSource ds) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(ds);
        factory.setTypeAliasesPackage("com.company.sub.entity");
        factory.getConfiguration().setMapUnderscoreToCamelCase(true);
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources("classpath*:mapper/slave/*.xml"));
        return factory.getObject();
    }

    @Bean(name = "secondarySessionTemplate")
    public SqlSessionTemplate template(@Qualifier("secondarySessionFactory") SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }
}

業務ロジックでのトランザクション検証

@Transactional アノテーションを使用して、アサートメントメソッド内から両方のリポジトリへの操作を行います。意図的な例外発生により、ロールバック動作が確認できる状態とします。

@Service
public class CrossDbOperationService {

    @Autowired
    private MasterMapper masterMapper;
    @Autowired
    private SlaveMapper slaveMapper;

    @Transactional(rollbackFor = Exception.class)
    public void syncData(String recordId, int amount) throws Exception {
        // 主 DB への書き込み
        MainRecord rec1 = new MainRecord();
        rec1.setId(recordId);
        rec1.setValue(amount);
        masterMapper.insert(rec1);

        // 従 DB への書き込み
        SubRecord rec2 = new SubRecord();
        rec2.setId(recordId);
        rec2.setValue(amount);
        slaveMapper.insert(rec2);

        // 強制的なエラー発生
        throw new RuntimeException("Simulated Failure");
    }
}

よくあるエラーと対処

PostgreSQL を使用中に「prepared transactions are disabled」というエラーが発生した場合、データベース設定ファイルを変更する必要があります。

Caused by: org.postgresql.util.PSQLException: 
ERROR: prepared transactions are disabled

これは postgresql.conf 内の max_prepared_transactions パラメータが有効になっていないことを示しています。Atomikos が分散トランザクションで二相コミットを行う際、この値が 0 では機能しません。以下の設定値に変更してデータベースを再起動してください。

# postgresql.conf
max_prepared_transactions = 100

通常はサーバー上の最大接続数に近い値に設定することが安定性のために推奨されます。

タグ: SpringBoot Atomikos JTA XA Transaction MyBatisPlus

10月4日 06:27 投稿