コンピューターサイエンス入門:理論編
残高転送のトランザクション風処理
この回でやること
送金の途中で起きる不整合を読み解き、原子性とコピーで守れる範囲を確かめます。
- 読む 約 7 分
- 最後にクイズ 1 問
引いたところで落ちると、お金が消える
Aの口座に1000円、Bに500円あります。Aから300円引き、Bに300円足す送金を考えます。
Python
accounts["A"] -= 300 # Aは700円になった
# ここでサーバーが再起動したとする
accounts["B"] += 300 # この行は実行されなかったAだけが減ると、合計1500円が700 + 500 = 1200円になります。誰も受け取っていない300円が消えています。各行の計算が正しくても、片方だけが保存されると送金全体は壊れます。
全部やるか、1つもやらないか
複数の変更をひとまとまりとして扱う仕組みがトランザクションです。全部の変更を確定するか、全部を取り消すかに分ける性質を原子性と呼びます。途中の変更が利用者に確定した結果として残らないことが狙いです。
データベースのACIDは、原子性、一貫性、分離性、永続性を表します。一貫性は残高などのルールを守り、分離性は同時処理の干渉を防ぎ、永続性は確定した変更を障害後も保つ性質です。この回では、まず送金の原子性を読みます。
残高が足りるかは、引く前に確認できます。足りなければ何も変えずに終了します。ただし、先に判定しても、その後の2つの更新の間でサーバーが止まる可能性は残ります。残高確認だけで原子性を保証できるわけではありません。
現場の話 本物のデータベースは、変更を復元するためのログも保存します。途中で電源が落ちても、再起動時に未確定の変更を取り消し、確定した変更を復元できるようにします。COMMITは変更の確定を指します。
コピーなら元の状態と結果を分けられる
小さな例では、口座の辞書をコピーし、そのコピーで送金後の残高を計算できます。元の辞書をそのまま変えると、返り値を使わなくても呼び出し元の残高まで変わります。
Python
original = {"A": 1000, "B": 500}
copied = dict(original) # 別の辞書を作る
copied["A"] -= 300
copied["B"] += 300
print(original) # {'A': 1000, 'B': 500}
print(copied) # {'A': 700, 'B': 800}元の合計も結果の合計も1500円です。結果を使うまでは、元の辞書は変わりません。dict は1段だけのコピーですが、ここでは値が数値なので足ります。値が別の辞書なら、内側は共有される点に注意します。
コピーの計算はデータベースのトランザクションそのものではありません。保存時の障害や同時送金には、DB側の仕組みも必要です。
例題で確かめる
Aが450円、Bが250円の状態で、Aから600円を送るとします。450 < 600なので残高不足です。引く前に中止し、Aは450円、Bは250円のままにします。合計も450 + 250 = 700円で変わりません。Aをマイナス150円にしてから失敗とする必要はありません。