やあ。今日もデータベースの深い森を探索しているかい?
今日は、分散システムの「最後の砦」とも言える2相コミット(2-Phase Commit、通称2PC)の話をしようと思う。
PostgreSQLを触っていると、いつかは「複数のデータベースにまたがって、一気に書き込みを確定させたい」という場面に遭遇するはずだ。でも、安易に手を出すと地獄を見る技術でもある。今日はその正体を、現場の視点から紐解いていくよ。
—
2相コミットって、ぶっちゃけ何なの?
一言で言うと、「みんなでせーので決めるための、慎重すぎる会議」だ。
例えば、ユーザーの残高データと、決済履歴データが別のDBサーバーにあるとする。もし決済履歴だけが書き込まれて、残高の引き落としが失敗したら? サービスとしては大惨事だよね。
これを防ぐために、トランザクションの確定を「準備フェーズ」と「コミットフェーズ」の2回に分けるのが2PCの基本だ。
1. 準備フェーズ(Prepare)
コーディネーター(司令塔)が各ノードに「準備はいい?」と聞く。ノードは「いけます!」と答えたら、その瞬間、どんなことがあってもコミットできるようにデータをディスクに書き込み、ロックを維持したまま待機する。
2. コミットフェーズ(Commit)
全員から「準備OK」の返事をもらえたら、コーディネーターが「じゃあ、確定して!」と号令をかける。もし一人でも「無理」と言ったら、全員にロールバックを命じる。
—
PostgreSQLでどう動く?
PostgreSQLでは、この仕組みが`PREPARE TRANSACTION`というコマンドとして用意されている。実務で使うならこんな感じだ。
— 1. トランザクションを開始して色々やる
BEGIN;
UPDATE accounts SET balance = balance – 1000 WHERE id = 1;
— 2. 準備状態(PREPARE)にする
— ‘gid’はグローバルなトランザクションID。これを後で参照する
PREPARE TRANSACTION ‘my_tx_001’;
この状態で、DBサーバーを再起動しても、データは「準備完了」の状態でディスクに保持され続ける。次に、確定させたいタイミングでこうするんだ。
— 3. 確定(コミット)する
COMMIT PREPARED ‘my_tx_001’;
もし何らかの理由で中止したければ、`ROLLBACK PREPARED ‘my_tx_001’;` を叩けばいい。
—
現場の先輩からの「耳の痛い」忠告
ここまで聞くと「便利じゃん!」と思うかもしれない。でも、一つだけ覚えておいてほしい。2PCは、諸刃の剣だ。
1. ロックの解放待ちという悪夢
準備フェーズに入ると、データはロックされたままになる。もしネットワークが切れたり、コーディネーターが落ちたりすると、そのロックは宙に浮いたままになる可能性がある。結果、他のトランザクションが軒並み待たされて、DB全体がフリーズする…なんてことは、大規模なシステムではあるあるなんだ。
2. 「分散トランザクション」より「分散させない設計」
僕は個人的に、できる限り2PCを避ける設計をおすすめしている。
例えば、「Sagaパターン」という考え方がある。2PCで無理やり整合性を取るのではなく、各サービスで独立してコミットし、失敗したら「補償トランザクション(打ち消し処理)」を実行して帳尻を合わせる手法だ。
最近のマイクロサービスアーキテクチャでは、こっちの方が圧倒的にスケーラビリティが高い。
—
まとめ:いつ使うべきか
2PCは、「どうしても強整合性が欠かせない、かつトランザクションの規模が小さい場合」の最終手段として取っておこう。
- 使うべき時: 銀行口座の振替のような、絶対にデータ不整合が許されない極めて限定的な場面。
- 避けるべき時: 高いスループットを求められるWeb APIの裏側、ノード間通信が不安定な環境。
もし君が今、2PCを導入しようか迷っているなら、一度立ち止まって「本当に非同期処理ではダメなのか?」を自問自答してみてほしい。
データベース技術は強力な武器だけど、一番の武器は「複雑さを極限まで減らす設計思想」だからね。
さて、今日はこの辺で。また何か詰まったら聞きに来てくれ。君のアーキテクチャが、明日も安定して動くことを祈っているよ!
コメント