データベース●●●○○

トランザクション

複数の操作を「全部成功か全部なかったことに」とまとめる整合性の仕組み。

概要

トランザクションは、複数のデータベース操作を「すべて成功するか、すべてなかったことにするか」のどちらかしかない不可分な一塊として扱う仕組みです。定番の例が銀行の振込で、「Aの口座から1万円引く」と「Bの口座に1万円足す」は2つの更新ですが、片方だけ成功した状態は絶対に許されません。トランザクションで括れば、途中でエラーや停電が起きても、中途半端な状態は残りません。

RDBMSを使う開発では、ECサイトの注文確定(在庫を減らす+注文レコードを作る+ポイントを付与する)のように、「一貫した意味を持つ複数の更新」が至るところに現れます。トランザクションはそうした更新の整合性を守る、業務システムの土台となる仕組みです。

なぜ生まれたか

データベースを更新する処理は、いつでも途中で失敗しえます。プログラムのバグ、ネットワーク切断、サーバの電源断——複数の更新のうち一部だけが書き込まれた瞬間に障害が起きれば、データは「引いたのに足されていない」ような矛盾した状態で固定されてしまいます。かつてはこうした事故からの復旧を、人手による突き合わせと修正で行うしかありませんでした。

さらに深刻なのが同時実行の問題です。複数の利用者が同じデータを同時に読み書きすると、互いの途中経過が混ざり合い、単独では正しい処理同士でも結果が壊れます。1970〜80年代のデータベース研究は、この「障害からの回復」と「同時実行の制御」を、アプリケーション側の努力ではなくデータベースの機能として保証する方法を確立しました。それがトランザクションであり、その保証内容を整理した性質がACIDです。開発者は「この範囲をひとまとめにしたい」と宣言するだけでよくなりました。

詳細

BEGIN・COMMIT・ROLLBACK

SQLでは、BEGIN(または START TRANSACTION)でトランザクションを開始し、一連の更新を行った後、すべて確定するなら COMMIT、なかったことにするなら ROLLBACK を発行します。COMMITが完了した瞬間、変更は永続化され、他の利用者からも見えるようになります。途中でエラーが起きた場合や明示的にROLLBACKした場合は、開始前の状態に完全に巻き戻ります。

トランザクションの一生は、BEGINで始まり、COMMITかROLLBACKのどちらかで必ず終わる状態遷移として整理できます。振込の例なら、口座Aと口座Bの更新が両方成功すればCOMMITで確定し、途中でエラーが起きればROLLBACKで口座Aの更新までもが元どおりになります。

BEGIN

UPDATE や INSERT を実行

COMMIT

ROLLBACK またはエラー

変更が確定し他からも見える

開始前の状態へ完全に巻き戻る

実行中

コミット済み

ロールバック済み

この巻き戻しを支えているのがログ(WAL: Write-Ahead Logging)です。データ本体を書き換える前に「何をどう変更するか」をログに記録しておくことで、クラッシュ後の再起動時にも「COMMIT済みの変更は再適用、未COMMITの変更は巻き戻し」という復旧が可能になります。

障害が起きたとき、トランザクションの有無で結果がどう変わるかを、送金デモで実際に確かめてみてください。

⚡ 体験: 障害が起きたとき、トランザクションは何を守るか
→ 3,000円 →

口座Aから口座Bへ3,000円を送金します。障害が起きたとき、トランザクションの有無で何が変わるか比べてみてください。

同時実行と分離レベル

トランザクションのもう一つの役割が、同時に走る他のトランザクションからの干渉を防ぐことです。ただし「完全に直列に実行したのと同じ」厳密さ(SERIALIZABLE)を常に求めると性能が犠牲になるため、SQL標準は干渉をどこまで許すかを4段階の分離レベル(READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE)として定義しています。レベルを緩めるほど、ダーティリード(未確定データが見える)、ノンリピータブルリード(同じ行を読み直すと値が変わる)、ファントムリード(検索し直すと行が増減する)といった現象が起こりえます。

実務ではPostgreSQLのデフォルトであるREAD COMMITTED、MySQL(InnoDB)のデフォルトであるREPEATABLE READで動かすことがほとんどですが、「残高を読んでから更新するまでの間に他の更新が割り込む」ようなロストアップデートはデフォルトでは防げません。SELECT ... FOR UPDATE による明示的なロックや、バージョン列で競合を検出する楽観的ロックを使い分けるのが定石です。

実務での落とし穴

ありがちな失敗の筆頭は、トランザクションを長く張りすぎることです。トランザクション中に外部APIの呼び出しやユーザーの入力待ちを挟むと、その間ロックが保持され続け、他の処理が軒並み待たされます。トランザクションは「短く、DB操作だけ」が原則です。また、複数のトランザクションが互いのロックを待ち合うデッドロックも避けられない現実で、RDBMSが検出して片方を強制ROLLBACKするため、アプリケーション側にはリトライの実装が求められます。

なお、この保証は基本的に1つのデータベースの中に閉じています。マイクロサービスのようにデータが複数のサービスに分かれる構成では単一のトランザクションで括ることができず、Sagaパターンやメッセージキューを使った結果整合性の設計が必要になります。「トランザクションの境界をどこに引くか」は、システム分割の設計そのものに関わる重要な問いです。