3秒でわかる
同じデータを複数の処理が同時に書き換えないよう、一時的に順番待ちを作るデータベースの仕組み。在庫や残高の矛盾を防ぎます。
もう少し詳しく
どういうものか
ロックは、ある行やテーブルを触っている処理がひとつ終わるまで、他の処理を待たせる仕組みです。ロックには大きく2種類あり、読み取り同士は同時に通す共有ロックと、書き込みのあいだ誰も入れない排他ロックに分かれます。多くのデータベースでは UPDATE や DELETE を実行した時点で、対象の行に排他ロックが自動でかかり、トランザクションが COMMIT か ROLLBACK で終わるまで解放されません。
なぜ必要か
在庫が1個の商品に、2人が同時に購入ボタンを押した場面を考えます。両方の処理が「残り1個」を読み、両方が「0個に更新」を書くと、2個売れたのに在庫は1減っただけになります。これはロストアップデートと呼ばれる典型的な事故です。ロックは、先に来た処理が読んで書き終えるまで後続を止めることで、読んだ値が書く直前に変わっていないことを保証します。
具体例
在庫を引き当てる場面では、読む時点で行を押さえておきます。
BEGIN;
-- 他のトランザクションはこの行に触れなくなる
SELECT stock FROM products WHERE id = 10 FOR UPDATE;
-- 在庫が足りることを確認してから減らす
UPDATE products SET stock = stock - 1 WHERE id = 10;
<a href="/glossary/commit" class="text-primary font-medium underline underline-offset-2 hover:text-primary-dark">COMMIT</a>;FOR UPDATE を付けずにただ SELECT しただけでは行は押さえられません。読んでからアプリ側で判断して書く、という流れの間に他の処理が割り込めてしまいます。
つまずきやすいところ
デッドロックが最も厄介です。処理Aが商品1を押さえて商品2を待ち、処理Bが商品2を押さえて商品1を待つと、どちらも永久に進みません。データベースは片方を強制終了させてエラーを返すので、アプリ側は再試行する作りにしておきます。防ぐ側の定石は、複数行を更新するとき必ずidの昇順で触ると決めておくことです。
もうひとつは、ロックを持ったまま外部APIを呼ぶ設計です。相手の応答が遅れるとその間ずっと行が塞がれ、無関係な利用者まで待たされます。トランザクションは短く保ち、通信は外に出します。
似た用語との違い
| 語 | 指すもの |
|---|---|
| ロック | 同時アクセスを物理的に順番待ちさせる仕組み |
| トランザクション | 複数の操作をまとめて成功か失敗かにする単位 |
| 楽観ロック | 実際には押さえず、更新時にバージョン番号の一致を確認する方式 |
楽観ロックは待ちが発生しないぶん軽く、衝突がまれな画面編集などに向きます。在庫のように衝突が前提の場所では、素直に行を押さえる方が安全です。