残高転送のトランザクション風処理
引いたところで落ちると、お金が消える
A の口座から 300 円引いて、B の口座に 300 円足す。送金の処理は、書いてしまえば 2 行です。
Python
accounts["A"] -= 300 # ここまでは実行された
# ここでサーバーが再起動した
accounts["B"] += 300 # 実行されなかったこの 2 行の間でプロセスが落ちると、A だけが 300 円減った世界が残ります。合わせて 1500 円あったはずのお金が 1200 円になっています。誰も受け取っていない 300 円が、どこにも無い状態です。
バグではありません。2 行とも書き方は正しく、順番も正しい。それでも壊れます。「片方だけ実行された状態」が存在しうる限り、いつか必ず起きます。
だから複数の書き換えは、全部やるか 1 つもやらないかのどちらかに固定します。これがトランザクションの原子性(Atomicity)です。データベースの ACID という 4 文字はこの A から始まり、残る 3 つは一貫性、分離性、永続性ですが、送金でまず効くのは A です。
途中まで書き換えない書き方にする
止まっても壊れないようにする道は 2 つあります。書き換える前の状態を控えておいて失敗したら戻すか、書き換えてよいと分かるまで 1 か所も触らないかです。自分の手で書くなら後者が確実です。
送金してよいかどうかは、引く前に判定できます。残高が足りるかは残高を見れば分かる。分かった時点で分岐して、足りなければ何も触らずに返してしまえば、「引いたのに足せなかった」状態が生まれる余地そのものが消えます。
現場の話 — 本物のデータベースは前者を選んでいます。変更を本体に反映する前に別のログへ書き出しておき、途中で電源が落ちても再起動時にログから復元します。
COMMITが一瞬で終わるように見えて裏で泥臭い仕事をしているのは、この控えを取っているからです。
渡された辞書を書き換えると、呼び出し元まで変わる
もう 1 つ、原子性を静かに壊すものがあります。引数で受け取った辞書を、そのまま書き換えることです。
Python
scores = {"数学": 80, "英語": 70}
def add_bonus(table):
table["数学"] += 10
return table
add_bonus(scores)
print(scores) # {'数学': 90, '英語': 70}返り値を受け取ってもいないのに、元の scores まで変わっています。辞書やオブジェクトは中身そのものが渡されるので、受け取った側の書き換えが呼び出し元に筒抜けになります。
これが混ざると、途中まで書き換えてから「やっぱりやめます」と元の値を返しても、呼び出し元の辞書はもう汚れています。先にコピーを作り、コピーだけを触って返してください。
Python
original = {"数学": 80}
copied = dict(original) # JavaScript なら { ...original }
copied["数学"] = 100
print(original) # {'数学': 80} 元は無事dict() やスプレッドで作れるのは 1 段だけのコピーです。今回の口座のように値が数値なら、これで足ります。
やってみよう
transfer(accounts, sender, receiver, amount) を完成させてください。
accountsはコピーしてから触る。渡された辞書そのものは最後まで変えないsenderの残高がamountに足りなければ、引きも足しもせずに返す- 足りるときだけ、
senderから引いてreceiverに足す
テストの期待値を見ると、送金が成功した場合も失敗した場合も、全口座の合計は入力と同じままです。引く処理と足す処理のどちらか一方だけが走ると、この合計が合わなくなります。
要件
- sender の残高が amount 未満なら何も変更せず返す(rollback 相当)
- 送金成功時は sender から amount を引き、receiver に amount を足す
- 入力 accounts を直接書き換えず、新しい辞書 / オブジェクトを返す
入出力例
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}