先に書くログに確定の印が届いた時点で、成功が決まる。落ちたら、印のある取引だけ本体へ反映し、印の無い取引は捨てる成功は、控え帳の確定の印で決まる本体を書き換える前に、まずログへ書く。落ちたら、印のある取引だけ本体へ反映する控え帳 (先に書くログ、WAL)本体を書き換える前に、まずここへ書く取引 A在庫を 1 個減らす取引 ACOMMIT確定の印取引 B残高を書き換える印なし印がディスクに届いてから、アプリに成功を返すここで電源が落ちる。メモリの中身は消える再起動したら、控え帳を読み直す取引 A は印がある本体のデータファイルへ反映する取引 B は印が無い無かったことにして捨てる先に書くログに確定の印が届いた時点で、成功が決まる。落ちたら、印のある取引だけ本体へ反映し、印の無い取引は捨てる
「巻き戻します」と言われても、信じる根拠がない
途中で落ちたトランザクションは再起動時に巻き戻される、と前の回で書きました。しかし電源が抜けた瞬間、メモリの中身は消えます。何を手がかりに「どこまでが確定で、どこからが未確定か」を判断しているのでしょうか。
答えは、本体を書き換える前にログを書いているからです。DB は更新を頼まれると、まず「この行をこう変える」という記録をログへ書きます。順番は次のとおりです。
- 変更内容をログに書く
- COMMIT のとき、ログに「ここまで確定」の印を書く
- その印がディスクに届いたことを確認してから、アプリに成功を返す
- 本体のデータファイルへは、その後で反映する
このログは WAL (Write-Ahead Log、先に書くログ) と呼ばれ、3 の「ディスクに届いたことの確認」には fsync という OS の命令が使われます。再起動時にはこのログを読み直し、確定の印があるものだけ本体へ反映し、印のないものは捨てます。だから、どのタイミングで落ちても半端な状態が残りません。原子性 (Atomicity) と永続性 (Durability) は、この 1 本のログで同時に支えられています。
順番が逆でない理由もはっきりしています。本体のデータファイルへの書き込みは、あちこちのページを書き換えるのでディスクの上を飛び回ります。ログは末尾に足していくだけなので、順番に書けます。返事を早く返したい部分だけを順次書き込みで済ませ、飛び回る方は後回しにする、という分担です。
落ちなくても、2 人来ると壊れる
BEGIN と COMMIT は、処理のまとまりを作る。読んで判断してから書くまでに相手を混ぜないかは、分離性の設定と書き方で決まるBEGIN と COMMIT で囲んでも、読んで判断してから減らすと、在庫は -1 になりうる在庫 1 個の品を、2 人が同時に買う。上から下へ時間が進む。在庫に CHECK の制約は無いとする処理 AA さんが買う処理 BB さんが買う在庫最後の 1 個BEGIN と COMMIT で囲んだだけBEGIN在庫を読む。1 個ある。買えると判断1BEGIN在庫を読む。1 個ある。買えると判断11 個減らす0COMMIT1 個減らす-1COMMIT2 人とも、読んだ 1 個を見て買えると判断した。囲むだけでは、判断してから減らすまでの間に、相手が割り込む分離性あり (行を待たせる例)BEGINロックを取って読む。1 個ある1BEGINロックを取れず、A が終わるまで待つ11 個減らす0COMMIT読む。0 個なので、減らさない0COMMIT片方ずつ順番に実行したのと同じ結果。方式によっては、待たずに片方が失敗して、やり直しになるBEGIN と COMMIT は、処理のまとまりを作る。読んで判断してから書くまでに相手を混ぜないかは、分離性の設定と書き方で決まる
BEGIN と COMMIT は、処理のまとまりを作る。読んで判断してから書くまでに相手を混ぜないかは、分離性の設定と書き方で決まるBEGIN と COMMIT で囲んでも、読んで判断してから減らすと、在庫は -1 になりうる在庫 1 個の品を、2 人が同時に買う。上から下へ時間が進む。在庫に CHECK の制約は無いとする処理 AA さんが買う処理 BB さんが買う在庫最後の 1 個BEGIN と COMMIT で囲んだだけBEGIN在庫を読む。1 個ある。買えると判断1BEGIN在庫を読む。1 個ある。買えると判断11 個減らす0COMMIT1 個減らす-1COMMIT2 人とも、読んだ 1 個を見て買えると判断した。囲むだけでは、判断してから減らすまでの間に、相手が割り込む分離性あり (行を待たせる例)BEGINロックを取って読む。1 個ある1BEGINロックを取れず、A が終わるまで待つ11 個減らす0COMMIT読む。0 個なので、減らさない0COMMIT片方ずつ順番に実行したのと同じ結果。方式によっては、待たずに片方が失敗して、やり直しになるBEGIN と COMMIT は、処理のまとまりを作る。読んで判断してから書くまでに相手を混ぜないかは、分離性の設定と書き方で決まる
残る 2 つは、何も落ちていないときの話です。
在庫が 1 個の商品を 2 人が同時に買おうとしたとします。どちらのトランザクションも「在庫は 1 個ある」と読み、どちらも 1 個減らして、在庫は -1 になります。片方ずつ順番に実行していれば起きなかった結果です。これを防ぐのが分離性 (Isolation) で、実現には行を待たせるか、更新前の値を別に取っておくかのどちらかが要ります。
一貫性 (Consistency) は、トランザクションが終わった時点で、DB に宣言したルールが全部守られていることです。主キーの重複、参照先のない外部キー、残高がマイナスになる更新。これらが残るくらいなら、DBMS はコミットを失敗させて全部巻き戻します。
ALTER TABLE accounts ADD CONSTRAINT chk_balance CHECK (balance >= 0);
ただし DBMS が見張れるのは、こうして宣言されたルールだけです。「出金額と入金額は一致する」のような業務の決まりごとは、アプリ側の責任として残ります。
速さのために、どれを弱めるか
4 つを厳密に守るほど遅くなります。いちばん効くのは、COMMIT のたびにログをディスクへ押し出す処理です。MySQL の innodb_flush_log_at_trx_commit を 1 から 2 に変えると、書き出しは OS のキャッシュまでで済み、ディスクへの反映は約 1 秒に 1 回にまとまります。MySQL のプロセスが落ちただけならデータは残りますが、サーバごと電源が落ちると直前の約 1 秒分のコミットが消えます。
つまみを回すこと自体は悪ではありません。危ないのは、何を捨てたかを知らずに回すことです。消えても翌日集計し直せるログと、消えたら返金が必要な決済とで、同じ設定を使う理由はありません。