トランザクションとACID — 同時実行の地獄と分離レベル
送金の途中で電源が落ちても残高が消えないのはなぜか、なぜ「テストは通るのに本番でだけ数字が合わない」バグが生まれるのかを前提知識ゼロから。原子性・分離レベル・MVCC・デッドロックを、実際のSQLとPostgreSQL/MySQLのパラメータ名まで降ろして解説します。
比喩: 送金の途中で停電したら
Aさんの口座から1万円を引き、Bさんの口座に1万円を足す。送金はこの2手です。1手目が終わった直後に停電したら、Aの残高は減り、Bの残高は増えていない。この世から1万円が消えます。誰も操作を間違えていないのに、帳簿だけが壊れる。
現実の会計はこれを「途中の状態を認めない」という約束で防ぎます。両方成立するか、どちらも無かったことにするか、そのどちらかしかない。 この約束に名前を付けたものがトランザクションです。
BEGIN;
UPDATE accounts SET balance = balance - 10000 WHERE id = 'A';
UPDATE accounts SET balance = balance + 10000 WHERE id = 'B';
COMMIT;
BEGIN から COMMIT までが1つの塊で、外から見える状態は「実行前」か「実行後」しかありません。中途半端な世界は存在しない——これがデータベースの一番大きな商品です。
ACIDという4文字
この商品が満たすべき性質は、頭文字を取って ACID と呼ばれます(1983年にHärderとReuterが整理して定着した言い方です)。
A — 原子性(Atomicity)。「これ以上分けられない」。2手がまとめて1手として扱われ、途中で失敗したら書き終えた1手目も巻き戻される。中間状態が外から観測できない、という保証です。
C — 一貫性(Consistency)。前後で決められたルールが破れていないこと。ACIDの中でこれだけ毛色が違い、データベースだけでは守りきれません。CHECK 制約や外部キーはデータベースが見ますが、「送金の前後で総額が変わらない」といった業務ルールは、そう書いた人が正しく書いたときにだけ守られます。アプリとの共同責任です。
D — 永続性(Durability)。COMMIT が成功を返したら、直後に電源が落ちても残ること。主にWAL(先行書き込みログ)で実現します。データ本体を書き換える前に「これから何をするか」をログに追記し、そのログだけを確実にディスクへ落とす。散らばったページを全部書くより、ログを末尾に追記するほうが速いからです。復旧時はログを読み直し、途中だったものを巻き戻して完了済みをやり直します。
I — 分離性(Isolation)。同時に走る他人の「途中」が見えないこと。理想は、全員が一列に並んで順に実行されたのと同じ結果になることです。
AとDは「壊れること」——電源が落ちる、プロセスが死ぬ——への備えで、仕組みが決まれば話は終わります。難しいのはIです。混むこと、つまり同じ行を何人もが同時にいじる状況では、分離性は4つの中で唯一性能と正面からぶつかります。全員を一列に並べれば異常はゼロになりますが、それでは同時に1人しか処理できない。安全と速度が真正面から取引になるので、Iだけは「守る/守らない」ではなく「どこまで諦めるか」を選ぶ設計項目になります。
しかも、諦めた分の代償は平常時には現れません。手元では1人で動かすので必ず正しく、テストも通る。混んだときだけ、在庫が合わない・二重に売れる・残高がずれる、という形で出てきます。「本番でだけ、たまに数字が合わない」バグの多くはここが出どころです。 ここから先は、そのIの話です。
同時に走らせると、こう壊れる
ダーティリード。まだ COMMIT していない他人の書きかけを読む。相手がその後 ROLLBACK すれば、それは一度も存在しなかった値です。
反復不能読み取り。同じトランザクション内で同じ行を2回読んだら値が変わっている。間に他人がコミットしたからです。
ファントムリード。同じ条件で範囲を2回検索したら行数が変わっている。1行の値ではなく「集合の輪郭」が動きます。
ロストアップデート。2人が読んで、計算して、書き戻す。後から書いたほうが先の更新を上書きします。
# アプリのコードで在庫を1減らす(危険な書き方)
stock = db.query("SELECT stock FROM items WHERE id=1") # 2人とも 10 を読む
db.execute("UPDATE items SET stock = %s WHERE id=1", stock - 1) # 2人とも 9 を書く
# 2個売れたのに在庫は 9。1個ぶんが消えた
読んで、考えて、書き戻す——この形は必ず疑ってください(同じ現象がスレッドでも起きることは並行処理を1からで扱っています)。
コメント
コメントにはログインが必要です