行やテーブルをロック - ロック (Locking)
楽観的ロック vs 悲観的ロック、SELECT ... FOR UPDATE、SKIP LOCKED / NOWAIT の使い分け
概念図
構文
sql
SELECT ... FOR UPDATE [NOWAIT | SKIP LOCKED];サンプル
FOR UPDATE で対象行を排他ロックし、競合更新を防ぐ
sql
-- 悲観的ロック: 行を排他ロックしてから更新
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- ここで他トランザクションは同じ行を待たされる
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
COMMIT;楽観的ロック vs 悲観的ロック
- 悲観的ロック (Pessimistic): 「他が触るはず」と想定し、先に行をロックしてから読み書きする。
SELECT ... FOR UPDATEが代表 - 楽観的ロック (Optimistic): 「ほとんど競合しない」と想定し、更新時に
version列やupdated_atを条件に含め、更新件数が 0 なら競合としてリトライ
書き込みが少なく衝突も稀なら楽観的、強い整合が必要・衝突が多いなら悲観的、と使い分けます。
sql
-- 楽観的ロック例 (version 列)
UPDATE products
SET name = 'New', version = version + 1
WHERE id = 10 AND version = 3;
-- 影響行数が 0 ならアプリ側でリトライFOR UPDATE と FOR SHARE
SELECT ... FOR UPDATE: 排他ロック (X ロック)。他セッションは読みも更新もブロックされる (RDBMS による)SELECT ... FOR SHARE(PostgreSQL / MySQL): 共有ロック (S ロック)。他セッションは読めるが更新はできない。参照整合性をトランザクション内で保ちたいときに使う- SQL Server では
WITH (UPDLOCK, HOLDLOCK)やWITH (ROWLOCK, XLOCK)のヒントで表現
NOWAIT と SKIP LOCKED
ロック待ちの挙動を変えるオプションです。
NOWAIT: ロックが取れなかったら待たずにエラー終了SKIP LOCKED: ロック中の行はスキップして、取れる行だけ返す。ジョブキューやワーカー分散で定番
PostgreSQL 9.5+ / MySQL 8.0+ / Oracle 18c+ でサポート(Oracle 11g〜12c では AQ 内部でのみ使われ、ユーザ SQL としては未ドキュメント)。SQL Server は READPAST ヒントで類似機能を提供します。
sql
-- ワーカーがジョブキューから 1 件取り出すパターン
BEGIN;
SELECT id, payload
FROM jobs
WHERE status = 'queued'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
-- 取れた行を処理してステータス更新
COMMIT;ロックの粒度
- 行ロック: 1 行単位。並行性が最大だがロック管理のオーバーヘッドあり
- ページ/ブロックロック: 複数行をまとめてロック (SQL Server のロックエスカレーション)
- テーブルロック: テーブル全体。DDL 実行時などに自動取得される
- ギャップロック / next-key ロック: MySQL InnoDB がファントム防止のために範囲もロックする
大量行を更新すると行ロックが自動的にテーブルロックへ昇格 (escalation) する RDBMS もあり、意図しない待機を招きます。
長すぎるロック保持の危険
ロックはトランザクションの COMMIT または ROLLBACK まで保持されます。以下は典型的なアンチパターンです。
- トランザクションを開いたまま HTTP レスポンスをユーザーに返してしまう
- バッチ処理で全行を
FOR UPDATEしてから重い計算を実行する - 外部 API 呼び出しをトランザクション中に行う
ロックを取る区間は最短に、外部 I/O や人間の待ちはトランザクション外で行うことが鉄則です。
