途中を見せない
注文を入れて在庫を減らす 2 つの文を、1 つの操作にする。2 セッションで「COMMIT 前は他人に見えない」を体験する。
- 2 つの文を「1 つの操作」にするには
- 途中の状態は、他の接続からどう見えるか
- 途中で失敗したら、どうなるか
実物: 2 本の接続
この章のラボは、A と B の 2 本の接続を持っている(第 0 章の SHOW PROCESSLIST で見た、サーバー側の 2 本のスレッドに対応する)。 ボタンで用意した文を A か B に流す。まず、説明なしで次の順に押してほしい: A-1 → A-2 → A-3 → B-1 → B-2。
A は注文を 1 件入れ、在庫を 1 減らした。それなのに B から見ると、注文数は 50 のまま、在庫も減っていないはずだ。 次に A-4(COMMIT)を押してから B-1 と B-2 をもう一度押す。今度は 51 件、在庫は 1 減っている。
これがトランザクションだ。BEGIN から COMMIT までの文は、ひとまとまりの操作として扱われ、 COMMIT するまで他の接続からは何も起きていないように見え、COMMIT した瞬間に全部が一度に見える。
ROLLBACK: なかったことにする
順に 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 すればいい。
途中で失敗したら
在庫が足りない注文を考える。在庫がマイナスにならないよう、制約を付けてから試す。
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 本の時間線にまとめる。
読んでいる間、世界は止まって見える
B-1 → B-2 → A-1 → B-3(まだ古い値)→ B-4 → B-2(新しい値)。 B のトランザクションの中では、最初に読んだ時点の状態をずっと読み続ける。途中で A が変えても、B の集計は矛盾しない。 MySQL の既定の動作(REPEATABLE READ という分離レベル)で、第二部 9 章で、書き込み同士がぶつかったときに何が起きるか(ロック)を見る。 表の今の値と、B に見える値を、2 本の帯で並べる。
教科書の言葉で言うと
- 原子性(Atomicity) — 全部成功か、全部なかったことか。ROLLBACK で見た。
- 一貫性(Consistency) — 制約を破った状態で終わらない。CHECK で見た。
- 分離性(Isolation) — 途中を他人に見せない。B から覗いて見た。
- 永続性(Durability) — COMMIT したら、直後に電源が落ちても残る。
頭文字で ACID。定義を暗記するより、上の 4 つのラボの体験を思い出せればいい。
後始末
ラボの「両方 ROLLBACK して切断」を押し、npm run seed:basics で作り直す。
BEGIN から COMMIT までは 1 つの操作。途中は他人に見えず、失敗すれば ROLLBACK で全部なかったことになる。
確認
BEGIN から COMMIT までの間、他の接続からは何が見えるか。
1 文がエラーになったとき、アプリのコードから ROLLBACK を出す必要があるのはなぜか。
トランザクションの中で、外部への呼び出しや人の入力を待ってはいけないのはなぜか。
次の章へ
第 0 章の「なぜファイルでないか」の 3 つのうち、2 つ(同時の書き換え、途中で落ちる)がこの章で片付いた。 残る 1 つ、「行が増えると探すのが遅くなる」に、次の章で初めて触れる。索引という実物を見る。