残高転送のトランザクション風処理
残高転送のトランザクション風処理
このレッスンで分かること
- リレーショナルデータベースが「銀行のお金を扱える」ほど信頼されているのは、
ACIDという 4 つの性質を満たすからです- この中でもとくに直感的に分かりやすいのが
Atomicityです- 図のポイント (テキスト併記)
残高転送のトランザクション風処理 とは
口座間の残高転送を、ACID の Atomicity(原子性)を満たすように実装します。残高不足ならロールバックして変更しません。
リレーショナルデータベースが「銀行のお金を扱える」ほど信頼されているのは、ACID という 4 つの性質を満たすからです。
- A =
Atomicity(原子性)処理は「全部成功」か「全部なかったこと」のどちらか - C =
Consistency(一貫性)処理の前後でデータの整合性が壊れない - I =
Isolation(分離性)同時実行しても互いに干渉しない(最も厳格なレベルでは直列実行と同じ結果になる。分離レベルによって保証の度合いが変わる) - D =
Durability(永続性)コミットしたデータは障害後も残る
この中でもとくに直感的に分かりやすいのが Atomicity です。「A さんから B さんに 1000 円送金する」処理を例にしましょう。
図のポイント (テキスト併記)
-
送金処理の途中でアプリが落ちて、A から引いた後で B に足す前に止まってしまうと、お金が「消える」
送金処理の途中でアプリが落ちて、A から引いた後で B に足す前に止まってしまうと、お金が「消える」。これを防ぐのが Atomicity。
コードでの考え方
プログラム上で Atomicity を表現する一番素朴な方法は、「変更前の状態を保存しておき、失敗したら戻す」 ことです。
Python
def transfer(accounts, sender, receiver, amount):
# 変更前の状態をコピーしておく
snapshot = dict(accounts)
try:
if accounts[sender] < amount:
raise ValueError("insufficient")
accounts[sender] -= amount
accounts[receiver] += amount
return accounts
except Exception:
# rollback
return snapshot
try / exceptで囲み、失敗時にsnapshotを返す。これが DB のBEGIN ... ROLLBACKをミニチュアで再現したもの。
Consistency も合わせて満たす
送金で守るべき不変条件(invariant)は 「合計残高が変わらない」 ことです。A から 1000 引いて B に 1000 足せば合計は同じ。これが Consistency。今回の課題でも、失敗した場合(残高不足)は 元の accounts をそのまま返す ことで、合計残高の不変性を保ちます。
JavaScript
function transfer(accounts, sender, receiver, amount) {
if (accounts[sender] < amount) {
return { ...accounts }; // 何もしない(rollback 相当)
}
return {
...accounts,
[sender]: accounts[sender] - amount,
[receiver]: accounts[receiver] + amount,
};
}よくある間違い
残高不足のチェックを忘れる と、A の残高がマイナスになって Consistency が壊れます。送金前に if accounts[sender] < amount を必ずチェックしましょう。
また、入力の accounts を直接書き換えてしまう と、呼び出し元の変数まで壊れてしまい、外側からみると Atomicity が破れた状態になります。Python なら dict(accounts) でコピー、JavaScript なら { ...accounts } でスプレッドして、新しいオブジェクトを返すのが安全です。
補足: 並行制御の難しさ
Isolation まで考えると、2 つの送金が同時に走るケースが厄介になります。例えば「A → B に 1000 円」と「A → C に 800 円」が同時に走ったとき、片方しか実際の残高チェックを通っていないと二重支出が起きます。本物の DB は 行ロック MVCC SELECT FOR UPDATE などで防いでいますが、今回はその手前の Atomicity + Consistency だけを実装します。
並行制御は CS の上級トピック。まずは「失敗時に元の状態に戻す」というシンプルな約束を覚えるのが第一歩。
Durability — 障害があっても消えない
4 つの性質の最後 Durability(永続性)は、commit 済みのトランザクションが 電源断やクラッシュの後でも残る ことを保証します。実装の仕組みとしては WAL(write-ahead log、先行ログ)を使って、変更を本体テーブルに反映する前に ログファイル に書き出し ます。万一クラッシュしても、再起動時にログを再生(replay)すれば失われた変更を復旧できます。今回のメモリ上ミニトランザクションでは Durability まで踏み込みませんが、本番 DB の commit が「ただメモリに書く」のではない理由はここにあります。
commitの裏ではWALfsynccheckpointなどの泥臭い仕事が走っている。Durability はその努力の総称。
やってみよう
accounts (口座名 → 残高の dict / Object)と sender receiver amount を受け取り、送金成功時は更新後の口座、失敗時は変更なしの口座を返す transfer を実装してください。
よくある質問
Q. トランザクションを使うべき場面は?
A. 「複数の更新を全部成功させるか、全部やめるか」が必要な場面で使います。銀行送金(出金と入金)、注文処理(在庫減算と注文記録)、ユーザー登録(複数テーブル更新)などが典型例です。原子性が保たれないと不整合が発生します。
Q. COMMIT と ROLLBACK の使い分けは?
A. 全処理が成功したら COMMIT で確定、途中でエラーが出たら ROLLBACK で全部取り消します。try/catch で囲み、try 末尾で COMMIT、catch 内で ROLLBACK が定型です。例外を握りつぶさず必ず ROLLBACK させる構造を意識しましょう。
Q. トランザクションを長く張ると何が問題ですか?
A. ロックが長時間保持され、他のトランザクションがブロックされて性能が落ちます。MVCC 系 DB では古いバージョンが消えず DB が肥大化します。1 トランザクションは数秒以内に終わるよう、不要な処理(HTTP 呼び出し等)を外に出すのが定石です。
次のレッスン
次は CAP の availability 模擬 に進みましょう。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- ACID トランザクション の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. ACID トランザクション とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- sender の残高が amount 未満なら何も変更せず返す(rollback 相当)
- 送金成功時は sender から amount を引き、receiver に amount を足す
- 入力 accounts を直接書き換えず、新しい辞書 / オブジェクトを返す
入出力例
test-cases.txt
transfer({"A":1000,"B":500}, "A", "B", 300) → {"A":700,"B":800}
transfer({"A":100,"B":500}, "A", "B", 300) → {"A":100,"B":500}
transfer({"A":300,"B":200}, "A", "B", 300) → {"A":0,"B":500}
transfer({"A":100,"B":200}, "A", "B", 0) → {"A":100,"B":200}
transfer({"A":500,"B":200,"C":100}, "A", "C", 250) → {"A":250,"B":200,"C":350}