大規模なOracle/PLSQLシステムでは、複数セッションが同じデータ資源へ同時にアクセスします。場当たり的なロック取得や無制限リトライは、デッドロック、待機時間の増大、スループット低下を招きます。この記事では、行ロック、楽観ロック、SKIP LOCKED、DBMS_LOCK、再試行、監視SQLまでを、実務で使える形に整理します。
トランザクションの基本は Oracleトランザクション完全ガイド、行ロック全般は テーブル・行ロック完全ガイド も参考にしてください。
SELECT FOR UPDATE NOWAITとSKIP LOCKEDの使い分け- バージョン列による楽観ロック
- 一意制約、制御テーブル、
DBMS_LOCKによるキー単位の直列化 ORA-00054、ORA-00060、ORA-08177の再試行設計- ロック待ちとブロッカーを調べる監視SQL
最初に結論:ロック順序・占有時間・再試行対象を固定する
同時実行制御では、すべての競合を消すのではなく、競合しても壊れない形に設計します。ロック取得順序を固定し、占有区間を短くし、再試行する例外を限定し、失敗時に安全に戻せる単位で処理します。
悲観制御:FOR UPDATEとNOWAIT/SKIP LOCKED
更新対象を先に占有したい場合は、SELECT ... FOR UPDATE を使います。待たずに失敗させたい場合は NOWAIT、複数ワーカーで未処理行を分散処理したい場合は SKIP LOCKED を使います。
-- ロック中ならすぐORA-00054にする SELECT id FROM task_queue WHERE id = :id FOR UPDATE NOWAIT;
ORA-00054 は、別セッションが対象リソースを保持していてロックを取得できない時の代表例です。詳しくは ORA-00054完全ガイド を確認してください。
ワーカー処理:SKIP LOCKEDで1件ずつ安全に拾う
キュー表を複数ワーカーで処理する場合は、ロック中の行を飛ばして前進する設計が有効です。SQL一文で無理にUPDATEするより、カーソルで対象行をロックしてから WHERE CURRENT OF で状態を変える方が読みやすく、安全に説明できます。
DECLARE
CURSOR c_task IS
SELECT id
FROM task_queue
WHERE status = 'READY'
ORDER BY priority DESC, id
FOR UPDATE SKIP LOCKED;
v_id task_queue.id%TYPE;
BEGIN
OPEN c_task;
FETCH c_task INTO v_id;
IF c_task%FOUND THEN
UPDATE task_queue
SET status = 'RUNNING',
picked_at = SYSTIMESTAMP
WHERE CURRENT OF c_task;
END IF;
CLOSE c_task;
COMMIT;
END;
/
SKIP LOCKED は取りこぼしではなく、後で再スキャンして拾う設計が前提です。処理中に落ちた行を戻すため、RUNNING のまま一定時間を超えた行を READY に戻す回収ジョブも用意します。カーソルと FOR UPDATE SKIP LOCKED の基本は PL/SQLカーソル完全ガイド も参考になります。
楽観制御:バージョン列でロストアップデートを防ぐ
競合が少ない画面更新では、最初からロックを取るより、更新時にバージョン列で衝突検知する方が向いています。期待バージョンと一致した時だけ更新し、更新できなければ利用者へ再読込を促します。
ALTER TABLE customers ADD (row_ver NUMBER DEFAULT 0 NOT NULL);
UPDATE customers
SET email = :new_email,
row_ver = row_ver + 1
WHERE id = :id
AND row_ver = :expected_ver;
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'version conflict');
END IF;
キー単位の直列化:一意制約と制御テーブル
特定キーに対して同時に1処理だけ通したい場合は、一意制約をミューテックスとして使う方法があります。ロック用テーブルにキーをINSERTし、競合時は一意制約違反で失敗させます。ただし、処理失敗時に制御行が残らないよう、トランザクション境界と後片付けを設計します。
CREATE TABLE key_locks (
lock_key VARCHAR2(200) PRIMARY KEY
);
BEGIN
INSERT INTO key_locks(lock_key) VALUES (:key);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
RAISE_APPLICATION_ERROR(-20010, 'key busy');
END;
-- クリティカルセクション
DELETE FROM key_locks WHERE lock_key = :key;
COMMIT;
Oracleには INSERT ... ON CONFLICT はありません。冪等化や重複吸収は、一意制約、MERGE、例外ハンドリングで設計します。一意制約違反の扱いは ORA-00001の原因と解決方法 と関連します。
DBMS_LOCK:ユーザー定義ロックで二重起動を防ぐ
ジョブやバッチ単位で明示的な排他が必要な場合は DBMS_LOCK が使えます。利用には権限付与が必要なことがあり、戻り値を見て成功、タイムアウト、デッドロック、パラメータエラーを分けます。
DECLARE
v_handle VARCHAR2(128);
v_result PLS_INTEGER;
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE('JOB:DAILY_AGG', v_handle);
v_result := DBMS_LOCK.REQUEST(
lockhandle => v_handle,
lockmode => DBMS_LOCK.X_MODE,
timeout => 0,
release_on_commit => TRUE);
CASE v_result
WHEN 0 THEN
NULL; -- success
WHEN 1 THEN
RAISE_APPLICATION_ERROR(-20000, 'lock timeout');
WHEN 2 THEN
RAISE_APPLICATION_ERROR(-20001, 'lock deadlock');
WHEN 3 THEN
RAISE_APPLICATION_ERROR(-20002, 'parameter error');
WHEN 4 THEN
RAISE_APPLICATION_ERROR(-20003, 'already own lock');
ELSE
RAISE_APPLICATION_ERROR(-20004, 'illegal lock handle');
END CASE;
-- クリティカルセクション
COMMIT; -- release_on_commit=TRUEならここで解放
END;
/
DBMS_LOCK.REQUEST の戻り値や release_on_commit の設計は重要です。詳しくは DBMS_LOCK完全ガイド、二重起動防止の実装は DBMS_LOCKで二重起動防止する方法 を確認してください。
トランザクション粒度:チャンクごとに安全に確定する
大量更新では、ロックを長時間持ち続けないようチャンク単位で確定します。IDの値で MOD(id, 50000) を見ると、IDが連番でない場合に意図通りになりません。処理件数カウンタでコミット間隔を管理します。
DECLARE
v_count PLS_INTEGER := 0;
BEGIN
FOR r IN (
SELECT id
FROM targets
WHERE processed = 'N'
ORDER BY id
) LOOP
SAVEPOINT sp_row;
BEGIN
UPDATE targets
SET processed = 'Y',
processed_at = SYSTIMESTAMP
WHERE id = r.id;
v_count := v_count + 1;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO sp_row;
END;
IF v_count >= 5000 THEN
COMMIT;
v_count := 0;
END IF;
END LOOP;
COMMIT;
END;
/
隔離レベル:READ COMMITTEDとSERIALIZABLE
更新系は通常の READ COMMITTED で短いロックにし、厳密な読み取り一貫性が必要な処理だけ SERIALIZABLE や READ ONLY を検討します。SERIALIZABLE で更新競合が起きると ORA-08177 が出るため、再試行設計が必要です。
-- 読み取り専用の一貫したスナップショット SET TRANSACTION READ ONLY; SELECT ... FROM report_source; COMMIT;
ORA-08177 の詳細は ORA-08177の原因と解決方法 を確認してください。
外部副作用:アウトボックスでDB外処理を分離する
メール送信やHTTP通知などの外部副作用は、データベースの行ロックでは守れません。業務DMLと同じトランザクションでアウトボックス表にイベントを記録し、別ワーカーが SKIP LOCKED で取り出して配送します。外部側にも冪等キーを渡し、二重送信を吸収できるようにします。
INSERT INTO outbox(event_key, event_type, payload, created_at) VALUES (:event_key, 'ORDER_CREATED', :json_payload, SYSTIMESTAMP); -- 配送ワーカー側 SELECT id FROM outbox WHERE sent_at IS NULL ORDER BY id FOR UPDATE SKIP LOCKED;
再試行戦略:対象例外を限定してバックオフする
再試行は、ロック競合、デッドロック、シリアライズ競合のような一時障害だけに限定します。未宣言の resource_busy や deadlock_detected を使うとコンパイルできないため、PRAGMA EXCEPTION_INIT でORA番号に紐付けます。
DECLARE
resource_busy EXCEPTION;
deadlock_detected EXCEPTION;
cannot_serialize EXCEPTION;
PRAGMA EXCEPTION_INIT(resource_busy, -54); -- ORA-00054
PRAGMA EXCEPTION_INIT(deadlock_detected, -60); -- ORA-00060
PRAGMA EXCEPTION_INIT(cannot_serialize, -8177); -- ORA-08177
v_try PLS_INTEGER := 0;
BEGIN
LOOP
v_try := v_try + 1;
BEGIN
-- 衝突しやすい更新
UPDATE targets
SET processed = 'Y'
WHERE id = :id;
COMMIT;
EXIT;
EXCEPTION
WHEN resource_busy OR deadlock_detected OR cannot_serialize THEN
ROLLBACK;
IF v_try < 5 THEN
DBMS_LOCK.SLEEP(POWER(2, v_try) * 0.2);
ELSE
RAISE;
END IF;
END;
END LOOP;
END;
/
ORA-00060 は最後のSQLだけがロールバックされるため、アプリ側でトランザクション全体をどう扱うか決める必要があります。詳しくは ORA-00060の原因と解決方法 を確認してください。
ジョブの同時実行制御
DBMS_SCHEDULER の設定だけに頼らず、ジョブ開始時に DBMS_LOCK や一意制約で二重起動を防ぎます。日次や時間帯単位のジョブでは、処理済み範囲、再開点、失敗行を記録し、再実行しても同じ最終状態へ収束するようにします。
- ジョブ単位の排他キーを決めた
- 同時起動時は待つのか、即終了するのか決めた
- 失敗時の再開点を記録している
- 外部通知はアウトボックスで分離している
- リトライ回数と上限時間を決めている
監視とトラブルシュート
障害発生後は、待機イベント、ロック待ちセッション、ブロッカーをすぐに確認できるようにします。運用では次のSQLを定番として手元に置いておくと初動が速くなります。
-- Concurrency待機中のセッション
SELECT sid,
serial#,
username,
event,
seconds_in_wait,
blocking_session
FROM v$session
WHERE state = 'WAITING'
AND wait_class = 'Concurrency';
-- ロック待ちと保有者の対応
SELECT waiter.sid AS waiter_sid,
holder.sid AS holder_sid,
waiter.type,
waiter.id1,
waiter.id2
FROM v$lock waiter
JOIN v$lock holder
ON waiter.type = holder.type
AND waiter.id1 = holder.id1
AND waiter.id2 = holder.id2
WHERE waiter.request > 0
AND holder.block = 1;
待機が増える場合は、実行計画、インデックス、パーティション設計、ロック順序、長いトランザクションを合わせて確認します。
まとめ
PL/SQLの同時実行制御では、ロック順序を固定し、占有時間を短くし、待つ処理と逃がす処理を使い分けることが重要です。NOWAIT、SKIP LOCKED、楽観ロック、DBMS_LOCK、一意制約を、用途ごとに選びます。
再試行は ORA-00054、ORA-00060、ORA-08177 のような一時競合に限定し、PRAGMA EXCEPTION_INIT で明示的にハンドリングします。監視SQLと運用ルールまで含めて設計すると、マルチセッション環境でも詰まりにくいシステムにできます。

