ロックと MVCC(同時実行制御)
複数のトランザクションが同じデータを同時に触っても壊れないよう、DB が裏で行う交通整理。ロックで待たせる方式と、バージョンを使って読み取りを止めない MVCC がある。
- 1.同時実行制御は、複数トランザクションの同時アクセスでデータが壊れないようにする仕組み。
- 2.悲観ロックは“先にロックして待たせる”、楽観ロックは“ぶつかったらやり直す”、MVCC は“バージョンを持ち読み取りを止めない”。
- 3.ロックが互いを待ち合うとデッドロック。DB は検知して片方を強制中断するので、アプリ側はリトライ前提で書く。
なぜ必要?
1人で使うだけなら順番に処理すればよく、競合は起きません。問題は同時に動くときです。たとえば在庫が1個の商品に、2人がほぼ同時に注文したとします。
-- A と B がほぼ同時に実行
SELECT stock FROM items WHERE id = 1; -- どちらも stock = 1 を読む
UPDATE items SET stock = stock - 1 WHERE id = 1; -- 両方が「1 - 1 = 0」にする
何も制御しなければ、2件の注文が通ったのに在庫は 0、つまり1個しかない商品が2個売れてしまいます。同時実行制御は、こうした更新の喪失(lost update)や、確定前のデータを読む(dirty read)といった異常を防ぐためにあります。隔離レベルの話とも直結します(→ トランザクション)。
ロックの基本:共有ロックと排他ロック
ロックは「このデータは今、私が使っているので待って」という札(ふだ)です。2種類を押さえれば十分です。
| 種類 | 別名 | 目的 | 両立できる相手 |
|---|---|---|---|
| 共有ロック | S ロック / read lock | 読んでいる間、他人に書き換えさせない | 他の共有ロックとは同時に持てる |
| 排他ロック | X ロック / write lock | 書き換える間、他人に読みも書きもさせない | 誰とも両立しない(独占) |
ポイントは「読み×読みは共存できるが、書きが絡むと独占」という点です。だから読み取りが多いシステムでは共有ロックが詰まりにくく、書き込みが多いほど排他ロックの取り合いで待ちが増えます。
悲観ロック vs 楽観ロック
「競合は起きるもの」と身構えるか、「めったに起きない」と楽観するかで戦略が変わります。
| 観点 | 悲観ロック(Pessimistic) | 楽観ロック(Optimistic) |
|---|---|---|
| 前提 | 衝突は頻繁に起きる | 衝突はめったに起きない |
| やり方 | 先にロックを取り、他を待たせる | ロックせず進め、コミット時に変化を検査 |
| 衝突時 | そもそも待つので衝突しない | 検知したら失敗させ、アプリがリトライ |
| 代表手段 | SELECT ... FOR UPDATE | バージョン番号 / 更新日時の照合 |
| 向く場面 | 競合が多い・在庫や残高など正確さ最優先 | 競合が少ない・Web 編集画面など待たせたくない |
悲観ロックは、読んだ時点で行をロックして他を締め出します。
BEGIN;
SELECT stock FROM items WHERE id = 1 FOR UPDATE; -- この行に排他ロック
UPDATE items SET stock = stock - 1 WHERE id = 1;
COMMIT; -- ここでロック解放。後続はここまで待たされる
楽観ロックは DB の機能というより設計パターンです。行に version 列を持たせ、「読んだときの版と同じなら更新する」とします。
-- 読んだ時点では version = 5 だった
UPDATE items SET stock = stock - 1, version = 6
WHERE id = 1 AND version = 5;
-- 更新件数が 0 なら、誰かが先に更新済み → 衝突。読み直してリトライ
悲観・楽観は S/X ロックのような別物のロックではなく、競合への構え方の違いです。楽観ロックはロックを取らないので、衝突がなければ高速。ただし衝突時のリトライ処理をアプリ側で必ず書く必要があります。これを忘れると更新が黙って消えます。
MVCC:読み取りをブロックしない仕組み
MVCC(Multi-Version Concurrency Control、多版同時実行制御)は、PostgreSQL・MySQL(InnoDB)・Oracle など多くの RDB が採用する中核の仕組みです。アイデアはシンプルで、データを上書きで消さず、更新のたびに新しいバージョンを作り、古い版も残しておくというもの。
各トランザクションは、自分が始まった時点の**スナップショット(その瞬間の世界)**を読みます。だから、
- 読み取りは書き込みをブロックしない:他人が更新中でも、自分は更新前の版をすぐ読める。
- 書き込みは読み取りをブロックしない:読んでいる人がいても、新しい版を作れる。
つまり「読み手」と「書き手」が互いに待たないのが最大の利点です。ロックだけの方式では、読み取りに共有ロックが必要で書き込みと衝突しがちでしたが、MVCC はそこを解消します。
MVCC でも、書き込み同士はぶつかります。同じ行を同時に更新しようとすれば、片方は行ロックを待つかエラーになります。MVCC が省けるのは主に「読み取りのための待ち」です。「MVCC だからロックは一切不要」は誤解で、書き込み競合・一意制約・SELECT ... FOR UPDATE ではロックが登場します。
古い版を残す方式なので、不要になった版を片付けないとディスクが膨らみます。PostgreSQL の VACUUM、InnoDB のパージがこの掃除役です。長時間開きっぱなしのトランザクションは「古い版をまだ読むかも」と見なされ掃除を妨げるため、トランザクションは短く保つのが鉄則です。
デッドロック:待ち合いの膠着
ロックを使う以上、避けて通れないのがデッドロックです。互いが相手の持つロックを待ち、永遠に進めなくなる状態を指します。
トランザクションA: 行1 をロック済み → 行2 を待つ
トランザクションB: 行2 をロック済み → 行1 を待つ
→ どちらも相手の解放待ち。永久に動かない
DB はこれを検知すると、片方を犠牲者(victim)に選んで強制的にロールバックし、もう片方を進めます。中断された側はエラー(例:デッドロック検出)を受け取ります。
デッドロックは設計不良とは限らず、同時実行があれば確率的に発生します。だからアプリはデッドロックエラーを受けたら自動でリトライする前提で書きます。さらに発生率を下げる定番策が「複数行をロックするときは、必ず同じ順序(例:ID 昇順)でアクセスする」こと。A は 1→2、B は 2→1 のように逆順で触るのが膠着の典型原因です。
つまずきポイント
トランザクションを長く開いたまま、ユーザー入力やネットワーク I/O を待つのは禁物です。その間ロックを握り続け、他を巻き込んで待たせ、デッドロックや性能劣化の温床になります。ロックは必要最小限の行を、できるだけ短時間だけが原則です。
まとめ:どれを選ぶ?
- 読み取りが多いなら、MVCC の恩恵が大きい(多くの RDB が標準で有効)。
- 競合が少ない更新(Web の編集画面など)は、楽観ロックで待たせず、衝突時だけリトライ。
- 競合が多く正確さが命(在庫・残高)なら、悲観ロック(
FOR UPDATE)で確実に直列化。 - いずれの方式でも、短いトランザクション・一定のロック順序・リトライ実装が安定運用の土台です。
OS レベルの排他制御(ミューテックスやセマフォ)とも考え方は地続きです(→ /os/concurrency-control/)。隔離レベルとの関係は トランザクション で深掘りできます。
データベース Article
ロックと MVCC(同時実行制御)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データベース
比較で見る軸
難易度: intermediate / カテゴリ: データベース / タグ数: 3
導入後に効く点
悲観ロックは“先にロックして待たせる”、楽観ロックは“ぶつかったらやり直す”、MVCC は“バージョンを持ち読み取りを止めない”。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- intermediate
- カテゴリ
- データベース
- タグ数
- 3
判断チェックリスト
- 自社の用途が「データベース / トランザクション」に近いか確認する。
- 強みである「同時実行制御は、複数トランザクションの同時アクセスでデータが壊れないようにする仕組み。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。