"SKIP LOCKED" による並行処理の最適化 (PostgreSQL/MySQL)
はじめに
複数のワーカープロセスが同じデータを並行処理する際、従来の SELECT ... FOR UPDATE
では行レベルロックの競合によりデッドロックや待機時間の問題が発生しがちでした。
PostgreSQL 9.5 と MySQL 8.0 で導入された SKIP LOCKED
オプションによって、並行処理が最適化される例を見ていきます。
従来の排他制御の問題点
典型的な問題
複数のワーカーが同時にタスクキューからジョブを取得する場合:
-- 従来のアプローチ(`SELECT ... FOR UPDATE`)
SELECT * FROM job_queue
WHERE status = 'pending'
ORDER BY created_at ASC
LIMIT 10
FOR UPDATE;
発生する問題:
- デッドロック: 複数のワーカーが異なる順序で同じ行をロックしようとする際に発生
- 行レベルロック競合: 同じ行を複数のワーカーが取得しようとすると、後続のワーカーは待機状態になる
- 効率の低下: 待機時間による処理能力の浪費と並行性の低下
SKIP LOCKED の仕組み
SKIP LOCKED
は「すでにロックされている行をスキップして、利用可能な行のみを返す」機能です。
動作例
初期状態でjobs テーブルに複数のpending タスクがある場合:
-
Worker A:
LIMIT 10 FOR UPDATE SKIP LOCKED
→ id=1-10 を取得・ロック -
Worker B:
LIMIT 10 FOR UPDATE SKIP LOCKED
→ id=11-20 を取得(1-10はスキップ)
各ワーカーは他のワーカーがロックしている行を待機せずにスキップし、利用可能な行のみをバッチで取得します。
基本的な実装パターン
ジョブキューでの使用
BEGIN;
-- 利用可能なジョブを取得
SELECT id, task_data FROM job_queue
WHERE status = 'pending'
ORDER BY priority DESC, created_at ASC
LIMIT 10
FOR UPDATE SKIP LOCKED;
-- 状態を更新
UPDATE job_queue
SET status = 'processing',
worker_id = 'worker-123'
WHERE id IN (取得したid一覧);
COMMIT;
SKIP LOCKED が適さないケース
SKIP LOCKED
は常に使えるものではなく、従来の FOR UPDATE
がふさわしい場面もあります:
1. 厳密な順序処理が必要
レコードを正確な順序で隙間なく処理する必要がある場合:
-- 請求書を厳密な番号順で処理(1, 2, 3, 4...)
SELECT * FROM invoices
WHERE status = 'pending'
ORDER BY invoice_number ASC
LIMIT 1
FOR UPDATE;
SKIP LOCKED
では、Worker Aが#1を処理中にWorker Bが#3を取得する可能性があり、順序が崩れる。
2. 依存関係のある処理
後続レコードが前のレコードの処理結果に依存する場合:
-- 銀行取引の順次処理(残高計算のため)
SELECT * FROM transactions
WHERE account_id = '12345' AND status = 'pending'
ORDER BY created_at ASC
LIMIT 1
FOR UPDATE;
3. 公平性の保証
SKIP LOCKED
では問題のあるレコードが継続的にスキップされる可能性があるため、すべてのレコードが確実に処理される必要がある場合は従来の方式が適している。
主要なユースケース
1. メッセージキューシステム
複数のメッセージプロセッサが並行してメッセージを配信(但し、順序を問わない場合)
2. バッチ処理・ETL
大量データの変換処理を複数ワーカーで分散
まとめ
SELECT ... FOR UPDATE SKIP LOCKED
は並行処理システムにおいて非常に強力な機能ですが、適用場面を選ぶ必要があります:
SKIP LOCKED が適している場面:
- 高いスループットと並行性 を重視
- 処理順序に柔軟性 がある
- デッドロック回避 が重要
従来のFOR UPDATE が適している場面:
- 厳密な順序処理 が必要
- 特定リソースの確保 が重要
- 公平性の保証 が求められる
Discussion