3秒でわかる
複数の SQL をひとかたまりとして扱い、全部成功か全部取り消しかに揃える単位。送金や在庫更新で中途半端な状態を防ぎます。
30秒図解
もう少し詳しく
どういうものか
トランザクションは、複数のデータベース操作を「まとめて成立させる」ための区切りです。BEGIN で始め、途中の更新をすべて確定させるなら COMMIT、どれか1つでも失敗したら ROLLBACK で開始前の状態に戻します。
その性質を4文字でまとめたものが ACID です。原子性は全部か無かに揃うこと、一貫性は制約を壊さないこと、独立性は同時に走る他の処理から途中経過が見えないこと、永続性はコミット後は障害が起きても残ることを指します。
なぜ必要か
口座 A から B へ1万円を移す処理は、A を減らす更新と B を増やす更新の2本です。A を減らした直後にサーバーが落ちたら、1万円が消えます。トランザクションで囲んでおけば、コミット前の落ち方はすべて無かったことになり、消失も二重計上も起きません。在庫の引き当てと注文の登録、ポイントの消費と付与など、対になる更新はすべて同じ理由で囲みます。
具体例
START TRANSACTION;
UPDATE accounts SET balance = balance - 10000 WHERE id = 1;
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
<a href="/glossary/commit" class="text-primary font-medium underline underline-offset-2 hover:text-primary-dark">COMMIT</a>;
-- どこかで異常があれば COMMIT のかわりに ROLLBACK;アプリ側から使う場合は、例外が出たら必ず戻す形にします。
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
await conn.execute("UPDATE stock SET qty = qty - ? WHERE id = ?", [1, itemId]);
await conn.execute("INSERT INTO orders (item_id) VALUES (?)", [itemId]);
await conn.commit();
} catch (e) {
await conn.rollback();
throw e;
} finally {
conn.release();
}つまずきやすいところ
囲めば安全、と考えて長く囲みすぎるのが典型的な失敗です。トランザクション中は行にロックがかかるため、その中で外部 API を呼んで数秒待つと、他の更新が全部詰まります。ロックを取る区間は最小にします。
自動コミットの存在も見落とされます。多くのクライアントは既定で1文ごとにコミットするため、BEGIN を書かなければ、2本の UPDATE は別々に確定します。囲んだつもりで囲めていない状態です。
独立性のレベルによって見え方が変わる点も混乱のもとです。MySQL の既定はリピータブルリードで、同じトランザクション中に2回読むと同じ結果が返りますが、PostgreSQL の既定はリードコミッティドで、他がコミットした結果が見えます。移植したら挙動が変わった、という事故はここから起きます。
覚え方
「途中で電源が抜けても困らない範囲」で囲む、と考えると、どこからどこまでを1つにするかを判断しやすくなります。