TCC分散トランザクションの実践:高スループット環境でのデータ整合性確保

1. 分散トランザクションの課題

モノリシックアーキテクチャ時代では、ACID特性によりデータベースのトランザクションを容易に管理できました。しかしマイクロサービス化が進む現代では、複数のサービス・データソース(MySQL、Redis、メッセージキューなど)にまたがる処理が必須となり、従来のローカルトランザクションでは対応不可です。

代表的な分散トランザクション手法には2PC、SAGA、ローカルメッセージテーブルがありますが、本稿ではTCC(Try-Confirm-Cancel)方式の実装方法を詳しく解説します。

2. TCCが必要な理由

2.1 2PCの性能限界

2PC(Two-Phase Commit)は強整合性を保証しますが、以下の致命的な欠点があります:

問題点影響
リソースロック全参加者のリソースが長時間ロックされるため、競合が発生
同期ブロッキングネットワーク遅延で処理全体が停止
単一障害点協調者障害でロックが解放されずサービス停止
パフォーマンス低下高負荷時にスループットが急激に減少

例えば決済処理で2PCを採用すると、送金元・先の口座がロックされ、他の取引が完全にブロックされるため、ECサイトのブラックフライデー対応には不向きです。

2.2 ハイパフォーマンス環境の実例

オンライン注文処理を想定します:

  • 在庫サービス:商品在庫の確保
  • 会計サービス:ユーザー残高の確認
  • 注文サービス:注文情報の登録

2PCの場合、すべてのサービスでリソースロックが発生するため、同一商品の同時購入やユーザーの他の取引が不可になります。

2.3 ハイブリッドリソース管理

現代システムでは以下の多様なリソースが使われます:

  • リレーショナルDB(MySQL、PostgreSQL)
  • NoSQL(MongoDB、Redis)
  • メッセージキュー(Kafka、RabbitMQ)
  • 外部API(決済、物流)

2PCはXAプロトコルに依存するため、RedisやKafkaの管理は不可。TCCはビジネスロジックで制御するため、すべてのリソースタイプに対応可能です。

3. TCCの仕組み

3.1 三段階処理の概要

TCCはビジネスロジックベースの分散トランザクション方式で、以下の3段階で処理を行います:

  1. Tryフェーズ:リソースの仮確保(在庫確認、残高チェック)
  2. Confirmフェーズ:実処理実行(在庫減算、金額引落)
  3. Cancelフェーズ:エラー時のロールバック(仮確保解除)

3.2 TCC vs 2PCの比較

項目2PCTCC
ロックメカニズムDB層の長期ロックビジネス層の短期予約
整合性強整合性最終整合性
パフォーマンス低速高速
リソース対応XA対応DBのみ任意のリソース可

3.3 二段階実行プロセス

実行パターン:

  • 正常系:全Try成功 → 全Confirm実行
  • 異常系:1つでもTry失敗 → 全Cancel実行

冪等性の確保が重要:ネットワーク再送でConfirm/Cancelが複数回呼ばれても同一結果を保証する必要があります。

3.4 代表的なエラー対処

  1. 空ロールバック:Try到達前にCancelが実行されるケース
    対策:Cancel実行時にステータスチェックを行う
  2. 冪等性対応:再送による重複処理
    対策:トランザクションIDで処理済み判定
  3. ビジネスハング:Cancel後にTryが到着
    対策:Try実行時に既にキャンセル済みか確認

4. 実装例:口座引落処理

4.1 シナリオ概要

ユーザーAからユーザーBへの100円送金を想定:

  • 高負荷耐性が必要
  • 残高不足時の処理拒否
  • エラー時のロールバック

4.2 データベース設計

-- ユーザー口座テーブル
CREATE TABLE user_account (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id VARCHAR(64) NOT NULL UNIQUE,
    available_balance DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 利用可能残高
    locked_balance DECIMAL(10,2) NOT NULL DEFAULT 0.00,    -- 仮ロック額
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id)
);

-- トランザクション履歴テーブル
CREATE TABLE transaction_history (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    global_tx_id VARCHAR(128) NOT NULL,
    user_id VARCHAR(64) NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    tx_status TINYINT NOT NULL DEFAULT 0, -- 0:TRY, 1:CONFIRM, 2:CANCEL
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_global_tx (global_tx_id),
    INDEX idx_user (user_id)
);

4.3 サービス実装コード

Seataフレームワークを使用した実装:

import io.seata.rm.tcc.TccAction;
import io.seata.rm.tcc.TccActionContext;

public class AccountDeduction implements TccAction {
    
    @TwoPhaseBusinessAction(name = "reserveFunds")
    public boolean prepare(BusinessActionContext ctx, 
                          @ActionContextParam("userId") String userId,
                          @ActionContextParam("amount") BigDecimal amount) {
        // 残高チェックと仮ロック
        if (checkBalance(userId, amount)) {
            lockFunds(userId, amount);
            saveTxHistory(userId, amount, TxStatus.TRY);
            return true;
        }
        return false;
    }

    @Commit
    public boolean commit(BusinessActionContext ctx) {
        String userId = ctx.getActionContext("userId");
        BigDecimal amount = ctx.getActionContext("amount");
        
        if (isAlreadyProcessed(userId, amount, TxStatus.CONFIRM)) {
            return true; // 冪等性対応
        }
        
        deductFunds(userId, amount);
        updateTxHistory(userId, amount, TxStatus.CONFIRM);
        return true;
    }

    @Rollback
    public boolean rollback(BusinessActionContext ctx) {
        String userId = ctx.getActionContext("userId");
        BigDecimal amount = ctx.getActionContext("amount");
        
        if (isAlreadyProcessed(userId, amount, TxStatus.CANCEL)) {
            return true; // 冪等性対応
        }
        
        releaseFunds(userId, amount);
        updateTxHistory(userId, amount, TxStatus.CANCEL);
        return true;
    }
}

タグ: Seata 分散トランザクション マイクロサービス Java SQL

7月17日 20:19 投稿