途中を見せない(トランザクション)
「注文の作成」と「在庫の減算」をひとつの安全な処理としてまとめる。2つのセッションを動かし、コミット前の変更が他から見えない仕組みを体験します。
- 2 つの文を「1 つの操作」にするには
- 途中の状態は、他の接続からどう見えるか
- 途中で失敗したら、どうなるか
2つのセッションを起動: 独立した接続環境
この章の実行環境では、A と B という 2 つの独立したデータベース接続を使用します(第 0 章の SHOW PROCESSLIST で確認した、サーバー側で並行して動作する 2 つのスレッドに相当します)。 ボタンをクリックして、用意された SQL 文を接続 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 側の変更もすべて破棄され、A-5 では元の値に復元されます。 COMMIT せずにロールバックすれば、行われた変更は最初から「何もなかったこと」になります。 第 5 章で紹介した「本番の手作業メンテナンスは BEGIN で始める」というルールは、まさにこの安全弁を利用するためです。影響行数が想定と違っていれば、即座に ROLLBACK で安全に巻き戻せます。
処理が途中で失敗したときの挙動
在庫が不足している場合の注文処理を考えてみましょう。在庫がマイナス値にならないよう、あらかじめテーブルに制約を追加しておきます。
A-1 → A-2 → A-3(制約エラー発生)→ A-4 → A-5 と進めてみます。 ここで注意すべきは、A-3 がエラーで失敗しても、A-2 で追加した注文レコードは自動的には消えないという点です(MySQL はクエリ単位でエラーになっても、トランザクション全体を自動でロールバックするわけではありません)。 そのため、アプリケーション側で明示的に ROLLBACK を発行し、途中で追加された注文を取り消す必要があります。 A-5 で注文件数が元の 50 件に戻っていることを確認してください。
// アプリケーションコードの基本形
begin()
try {
insert(注文)
update(在庫を減らす) // ここで例外が発生した場合
commit()
} catch (error) {
rollback() // 直前の注文もすべて巻き戻して整合性を保つ
}コミットによる変更の確定と、ロールバックによる取り消しのタイムラインを図で確認してみましょう。
読み取りの一貫性: 読んでいる間、世界は止まって見える
B-1 → B-2 → A-1 → B-3(まだ古い値のまま)→ B-4 → B-2(最新の値に更新)という順序で実行してみます。 トランザクションを開始した接続 B からは、そのトランザクション内で最初にデータを読み取った時点のスナップショットが一貫して見え続けます。 途中で別の接続 A がデータを更新してコミットしても、接続 B の集計処理に矛盾が生じることはありません。 これは MySQL のデフォルトの動作(REPEATABLE READ 分離レベル)による保護です。
ACID 特性(信頼性の 4 原則)
- 原子性(Atomicity): すべて成功するか、すべて取り消されるかのどちらかであり、中途半端な状態を残さない。
- 一貫性(Consistency): NOT NULL や CHECK 制約などの整合性ルールを破った不正な状態では処理を終了しない。
- 分離性(Isolation): 処理の途中の状態を他の接続から見せない(独立性を保つ)。
- 永続性(Durability): 一度 COMMIT されたデータは、その直後にシステム障害や停電が発生しても確実に失われない。
これら 4 つの頭文字を取って ACID 特性 と呼びます。単語を丸暗記するよりも、先ほどの 2 つの接続での検証体験と結びつけて理解しておくことが重要です。
後始末
ラボの「両方 ROLLBACK して切断」を押し、npm run seed:basics で作り直す。
BEGIN から COMMIT までは 1 つの操作。途中は他人に見えず、失敗すれば ROLLBACK で全部なかったことになる。
確認
BEGIN から COMMIT までの間、他の接続からは何が見えるか。
1 文がエラーになったとき、アプリのコードから ROLLBACK を出す必要があるのはなぜか。
トランザクションの中で、外部への呼び出しや人の入力を待ってはいけないのはなぜか。
次の章へ
第 0 章で触れた「スプレッドシートでは破綻する課題」のうち、2 つ(同時の更新衝突、処理途中のクラッシュ)がこの章で解決しました。 残る1つの課題「データ行数が増加すると検索が遅くなる問題」に、次の章でいよいよ切り込みます。インデックスの仕組みとその正体を実際に確かめていきましょう。