SQL Lab
第一部 DB 基礎編 · 第 7 章 · 約 30

途中を見せない

注文を入れて在庫を減らす 2 つの文を、1 つの操作にする。2 セッションで「COMMIT 前は他人に見えない」を体験する。

この章で答える問い
  1. 2 つの文を「1 つの操作」にするには
  2. 途中の状態は、他の接続からどう見えるか
  3. 途中で失敗したら、どうなるか

実物: 2 本の接続

この章のラボは、A と B の 2 本の接続を持っている(第 0 章の SHOW PROCESSLIST で見た、サーバー側の 2 本のスレッドに対応する)。 ボタンで用意した文を A か B に流す。まず、説明なしで次の順に押してほしい: A-1 → A-2 → A-3 → B-1 → B-2。

A で注文を入れて在庫を減らし、B から覗く
見る場所: B-1 と B-2 の結果。A-4 のあとで、もう一度 B-1 と B-2
セッション A
セッション B
この版は記録の再生。

A は注文を 1 件入れ、在庫を 1 減らした。それなのに B から見ると、注文数は 50 のまま、在庫も減っていないはずだ。 次に A-4(COMMIT)を押してから B-1 と B-2 をもう一度押す。今度は 51 件、在庫は 1 減っている。

これがトランザクションだ。BEGIN から COMMIT までの文は、ひとまとまりの操作として扱われ、 COMMIT するまで他の接続からは何も起きていないように見え、COMMIT した瞬間に全部が一度に見える。

ROLLBACK: なかったことにする

やめる
見る場所: A-3 では減って見えるが、A-4 のあとの A-5 で元に戻っている
セッション A
セッション B
この版は記録の再生。

順に A-1 → A-2 → A-3 → B-1 → A-4 → A-5。A-3 では A 自身には 0 に見える。B-1 では B には元の値に見える。 A-4 の ROLLBACK で、A 自身にも元の値に戻る。COMMIT せずにやめれば、何も起きなかったことになる。 第 5 章の「本番の手作業は BEGIN の中で」はこのためだ。影響行数が想定と違えば ROLLBACK すればいい。

途中で失敗したら

在庫が足りない注文を考える。在庫がマイナスにならないよう、制約を付けてから試す。

在庫は 0 以上、という制約
期待: エラーなく終わる
在庫不足の注文
見る場所: A-3 が CHECK 制約でエラーになる。A-4 で全部取り消す
セッション A
セッション B
この版は記録の再生。

A-1 → A-2 → A-3(エラー)→ A-4 → A-5。A-3 が失敗しても、A-2 の注文はまだ入ったままになっている(MySQL は 1 文が失敗してもトランザクションを自動では取り消さない)。 だから A-4 で ROLLBACK して、注文も消す。A-5 で 50 に戻っていることを確認する。 アプリのコードは「どれか 1 文が失敗したら必ず ROLLBACK」と書く。

// 擬似コード
begin()
try {
  insert(注文)
  update(在庫を減らす)     // ここで失敗したら
  commit()
} catch {
  rollback()               // 注文も消える
}

最初のラボ(COMMIT まで見えない → 一度に見える)と、このラボ(失敗 → ROLLBACK)を 1 本の時間線にまとめる。

BEGININSERT 注文UPDATE 在庫 −1COMMITB から見える値: 注文数 50 / 在庫 55(変更前のまま)51 / 54 が一度にUPDATE が失敗ROLLBACK注文は入ったまま → ROLLBACK で注文も消え、50 / 55 に戻るサーバーが落ちたときも、COMMIT 前の変更は同じく残らない。
BEGIN から COMMIT までは他人に見えず、ROLLBACK すれば(サーバーが落ちても)全部なかったことになる。

読んでいる間、世界は止まって見える

B が読んでいる間に A が変える
見る場所: B-2 と B-3 は同じ値。B-4 のあとの B-2 で初めて変わる
セッション A
セッション B
この版は記録の再生。

B-1 → B-2 → A-1 → B-3(まだ古い値)→ B-4 → B-2(新しい値)。 B のトランザクションの中では、最初に読んだ時点の状態をずっと読み続ける。途中で A が変えても、B の集計は矛盾しない。 MySQL の既定の動作(REPEATABLE READ という分離レベル)で、第二部 9 章で、書き込み同士がぶつかったときに何が起きるか(ロック)を見る。 表の今の値と、B に見える値を、2 本の帯で並べる。

AUPDATE → 777(BEGIN なし = 即確定)BBEGINSELECT → 37SELECT → 37COMMITSELECT → 777表の今の値37777(A-1 の時点で確定)B に見える値37 のまま(最初の SELECT の時点)777(COMMIT の後)
B のトランザクションの中では最初に読んだ時点の値を読み続け、A が確定させても B が COMMIT するまで 777 は見えない。

教科書の言葉で言うと

  • 原子性(Atomicity) — 全部成功か、全部なかったことか。ROLLBACK で見た。
  • 一貫性(Consistency) — 制約を破った状態で終わらない。CHECK で見た。
  • 分離性(Isolation) — 途中を他人に見せない。B から覗いて見た。
  • 永続性(Durability) — COMMIT したら、直後に電源が落ちても残る。

頭文字で ACID。定義を暗記するより、上の 4 つのラボの体験を思い出せればいい。

後始末

ラボの「両方 ROLLBACK して切断」を押し、npm run seed:basics で作り直す。

BEGIN から COMMIT までは 1 つの操作。途中は他人に見えず、失敗すれば ROLLBACK で全部なかったことになる。

確認

BEGIN から COMMIT までの間、他の接続からは何が見えるか。
何も起きていないように見える。ひとまとまりの操作として扱われ、COMMIT した瞬間に全部が一度に見えるようになる。
1 文がエラーになったとき、アプリのコードから ROLLBACK を出す必要があるのはなぜか。
MySQL は 1 文が失敗してもトランザクションを自動では取り消さないから。先に成功した文がそのまま残るので、どれか 1 文が失敗したら必ず ROLLBACK と書く。
トランザクションの中で、外部への呼び出しや人の入力を待ってはいけないのはなぜか。
待っている間もトランザクションは開いたままで、触った行を他の人が触れなくなるから。BEGIN から COMMIT までの時間は短く保つ。

次の章へ

第 0 章の「なぜファイルでないか」の 3 つのうち、2 つ(同時の書き換え、途中で落ちる)がこの章で片付いた。 残る 1 つ、「行が増えると探すのが遅くなる」に、次の章で初めて触れる。索引という実物を見る。