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段階で処理を行います:
- Tryフェーズ:リソースの仮確保(在庫確認、残高チェック)
- Confirmフェーズ:実処理実行(在庫減算、金額引落)
- Cancelフェーズ:エラー時のロールバック(仮確保解除)
3.2 TCC vs 2PCの比較
| 項目 | 2PC | TCC |
|---|---|---|
| ロックメカニズム | DB層の長期ロック | ビジネス層の短期予約 |
| 整合性 | 強整合性 | 最終整合性 |
| パフォーマンス | 低速 | 高速 |
| リソース対応 | XA対応DBのみ | 任意のリソース可 |
3.3 二段階実行プロセス
実行パターン:
- 正常系:全Try成功 → 全Confirm実行
- 異常系:1つでもTry失敗 → 全Cancel実行
冪等性の確保が重要:ネットワーク再送でConfirm/Cancelが複数回呼ばれても同一結果を保証する必要があります。
3.4 代表的なエラー対処
- 空ロールバック:Try到達前にCancelが実行されるケース
対策:Cancel実行時にステータスチェックを行う - 冪等性対応:再送による重複処理
対策:トランザクションIDで処理済み判定 - ビジネスハング: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;
}
}