Seataによる分散トランザクションの実践ガイド

マイクロサービス環境では、データベースやサービスが分割されることで、従来の単一DBトランザクションでは対応できない「複数サービス・複数DBにまたがる一貫性」の問題が生じます。この課題を解決するための代表的なフレームワークがSeataです。

分散トランザクションの基本戦略

  • 強一貫性:全ノードが成功か失敗かを同期的に保証。性能は犠牲になるが、整合性は確実。
  • 最終一貫性:一時的な不整合を許容し、時間経過で整合性を回復。可用性とスケーラビリティに優れる。

Seataが提供する4つのモード

1. XAモード(強一貫性)

データベースのXAプロトコルを利用。第一段階でロックを保持するため性能は劣るが、ACIDを厳密に保証。

// グローバルトランザクション開始
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
tx.begin();

try {
    // 分岐トランザクション実行
    branchService.execute();
    tx.commit(); // 全分岐成功 → 全体コミット
} catch (Exception e) {
    tx.rollback(); // いずれか失敗 → 全体ロールバック
}

2. ATモード(最終一貫性・デフォルト)

ローカルトランザクション+undoログによる補償型。第一段階で即時コミットし、失敗時にundoログでロールバック。

グローバルロックによる同時実行制御
他のトランザクションが同じレコードを更新しようとした場合、グローバルロックを取得できなければ一定時間リトライ後失敗。これにより「ダーティライト」を回避。

3. TCCモード(高性能・コード侵入あり)

ビジネスロジックをTry/Confirm/Cancelの3段階に分割。リソース予約→確定or取消の明示的制御。

@TwoPhaseBusinessAction(name = "transfer", commitMethod = "confirm", rollbackMethod = "cancel")
public boolean tryTransfer(@BusinessActionContextParameter(paramName = "amount") int amount) {
    // Try: 残高チェック & 仮凍結
    return accountService.reserve(amount);
}

public boolean confirm(BusinessActionContext context) {
    // Confirm: 実際の出金処理
    return accountService.deduct((int)context.getActionContext("amount"));
}

public boolean cancel(BusinessActionContext context) {
    // Cancel: 凍結解除
    return accountService.release((int)context.getActionContext("amount"));
}

4. Sagaモード(長時間トランザクション向け)

各ステップがローカルコミットを行い、失敗時はそれまで成功したステップを逆順で補償。非同期処理や外部API呼び出しに適す。

環境構築手順(Spring Cloud + Nacos + Docker)

Docker Compose設定

version: '3'
services:
  seata-server:
    image: seataio/seata-server:1.5.2
    ports:
      - "8091:8091"
      - "7091:7091"
    environment:
      SEATA_CONFIG_NAME: file:/seata-config/registry
    volumes:
      - ./config:/seata-config
      - ./logs:/root/logs

Nacos設定例(seataServer.properties)

store.mode=db
store.db.url=jdbc:mysql://db-host:3306/seata_db
store.db.user=seata_user
store.db.password=secure_password

service.vgroupMapping.my_tx_group=default
client.rm.reportSuccessEnable=true

必要テーブル作成(MySQL)

-- 各業務DBに配置
CREATE TABLE undo_log (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  xid VARCHAR(100) NOT NULL,
  branch_id BIGINT NOT NULL,
  rollback_info LONGBLOB NOT NULL,
  UNIQUE KEY uk_xid_branch(xid, branch_id)
);

-- Seata管理用DB
CREATE TABLE global_table (...);
CREATE TABLE branch_table (...);
CREATE TABLE lock_table (...);

アプリケーション統合

Maven依存

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>

application.yml設定

seata:
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
    grouplist:
      default: seata-server:8091
  registry:
    type: nacos
    nacos:
      server-addr: nacos-server:8848
      namespace: seata-ns-id

トランザクション制御

@GlobalTransactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
    // ローカル更新
    accountMapper.debit(from.getId(), amount);
    
    // 遠隔サービス呼び出し(失敗時は全体ロールバック)
    remoteAccountService.credit(to.getId(), amount);
}

よくあるトラブルと対策

問題:Feignのfallbackで例外を握りつぶし、トランザクションがロールバックされない。

解決策:AOPでfallbackメソッドを監視し、手動ロールバックを実行。

@Aspect
@Component
public class SeataFallbackAspect {
    @After("execution(* *..*Fallback.*(..))")
    public void handleFallback(JoinPoint jp) {
        String xid = RootContext.getXID();
        if (xid != null) {
            GlobalTransaction tx = GlobalTransactionContext.reload(xid);
            tx.rollback(); // 手動ロールバック
        }
    }
}

タグ: Seata 分散トランザクション SpringCloud Nacos Docker

5月21日 03:39 投稿