DBMS_LOCK は、Oracle Database内でユーザー定義ロックを扱うためのPL/SQL標準パッケージです。テーブル行を直接ロックするのではなく、任意の名前に対して排他ロックや共有ロックを取得できます。そのため、PL/SQLバッチの二重起動防止、ジョブ単位の排他、外部連携処理の同時実行抑止に向いています。
ただし、release_on_commit の選び方を間違えると、途中 COMMIT でロックが外れてしまい、守っているつもりの処理が並行実行される危険があります。この記事では、PL/SQL実装パターンに絞って、ALLOCATE_UNIQUE、REQUEST、RELEASE、戻り値判定、ログ設計、DBMS_SCHEDULER連携まで実務向けに整理します。DBMS_LOCK全体の仕様は Oracle DBMS_LOCK完全ガイド も参考にしてください。
- DBMS_LOCKで二重起動を防ぐ基本形
ALLOCATE_UNIQUEとロック名設計REQUEST/RELEASEの戻り値release_on_commitと途中COMMITの注意点- 本番向けの明示RELEASEテンプレート
- 実行ログ・スキップ理由・処理時間の記録
- 行ロック、DBMS_SCHEDULER、DBMS_ALERTとの使い分け
DBMS_LOCKで守る対象
DBMS_LOCKで守るべき対象は、テーブルの1行そのものではなく、論理的な資源です。たとえば「月次請求バッチ」「顧客ID単位の外部API連携」「ファイル出力処理」「キャッシュ更新処理」のように、業務上同時に動かしたくない処理を名前で表します。
SELECT FOR UPDATE や一意制約で守ります。DBMS_SCHEDULER と組み合わせます。行ロックやトランザクション全体の考え方は テーブル・行ロック完全ガイド、OracleのCOMMIT/ROLLBACKや SELECT FOR UPDATE は Oracleトランザクション完全ガイド も参考になります。
必要な権限
DBMS_LOCK を使うには、実行ユーザーにパッケージ実行権限が必要です。本番ではアプリケーション実行スキーマへ直接広く権限を配るのではなく、ロック用パッケージを作って権限境界を絞る設計も検討します。
-- DBAまたは権限を持つユーザーで実行する例 GRANT EXECUTE ON DBMS_LOCK TO app_owner; -- 実務では app_lock パッケージだけを公開し、 -- DBMS_LOCK の直接利用箇所を集約する設計も有効
ALLOCATE_UNIQUEでロック名からハンドルを取得する
DBMS_LOCK.ALLOCATE_UNIQUE は、文字列のロック名からロックハンドルを取得します。同じロック名を使えば、別セッションでも同じ論理ロックを参照できます。Oracle公式ドキュメントでは、新しいロック名が初めて割り当てられると DBMS_LOCK_ALLOCATED に情報が格納されると説明されています。そのため、毎回ランダムなロック名を作るのではなく、安定した名前を設計します。
DECLARE
v_handle VARCHAR2(128);
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE(
lockname => 'APP:BATCH:MONTHLY_BILLING',
lockhandle => v_handle,
expiration_secs => 86400
);
DBMS_OUTPUT.PUT_LINE('lock handle=' || v_handle);
END;
/
APP名:資源種別:キー のように規則を決めておくと運用しやすくなります。REQUESTで排他ロックを取得する
ロックを取得するには DBMS_LOCK.REQUEST を使います。二重起動防止では、X_MODE の排他ロックを、timeout => 0 で即時取得します。取得できなければ他セッションが実行中と判断し、処理をスキップします。
DECLARE
v_handle VARCHAR2(128);
v_result PLS_INTEGER;
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE('APP:BATCH:DAILY_EXPORT', v_handle);
v_result := DBMS_LOCK.REQUEST(
lockhandle => v_handle,
lockmode => DBMS_LOCK.X_MODE,
timeout => 0,
release_on_commit => TRUE
);
IF v_result = 0 THEN
DBMS_OUTPUT.PUT_LINE('lock acquired');
-- ここに短い処理を書く
COMMIT; -- release_on_commit => TRUE なのでここで解放
ELSIF v_result = 1 THEN
DBMS_OUTPUT.PUT_LINE('already running');
ELSE
DBMS_OUTPUT.PUT_LINE('lock request failed: ' || v_result);
END IF;
END;
/
この例は短い処理向けです。途中で COMMIT しない処理であれば扱いやすいですが、バッチ中に複数回COMMITする処理では危険です。次の章がこの記事で一番大事です。
途中COMMITがあるならrelease_on_commitをFALSEにする
release_on_commit => TRUE は、COMMIT または ROLLBACK でロックを解放します。つまり、処理の途中でコミットするバッチでは、最初のコミット時点でロックが外れます。その後に別セッションが同じジョブを開始できてしまうため、二重起動防止としては不十分になります。
-- 危険な例: 途中COMMITでロックが外れる v_result := DBMS_LOCK.REQUEST( lockhandle => v_handle, lockmode => DBMS_LOCK.X_MODE, timeout => 0, release_on_commit => TRUE ); -- ここでロック取得成功 UPDATE work_table SET status = 'PROCESSING'; COMMIT; -- この時点でロックが解放される -- この後の長い処理中に、別セッションが同じロックを取得できる可能性がある
途中COMMITがある長時間バッチでは、release_on_commit => FALSE にして、最後に必ず RELEASE します。例外時にも解放を試みるよう、テンプレート化しておくのが安全です。
本番向けテンプレート: 明示RELEASE方式
次は、途中COMMITがあるバッチでも二重起動を防ぎやすいテンプレートです。ロック取得成功時だけ本処理へ進み、正常時・異常時のどちらでも RELEASE を呼びます。
DECLARE
v_handle VARCHAR2(128);
v_result PLS_INTEGER;
v_released PLS_INTEGER;
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE('APP:BATCH:MONTHLY_BILLING', v_handle);
v_result := DBMS_LOCK.REQUEST(
lockhandle => v_handle,
lockmode => DBMS_LOCK.X_MODE,
timeout => 0,
release_on_commit => FALSE
);
IF v_result = 0 THEN
BEGIN
-- 途中COMMITがあってもロックは保持される
UPDATE billing_work
SET status = 'PROCESSING'
WHERE status = 'READY';
COMMIT;
-- 本処理
NULL;
v_released := DBMS_LOCK.RELEASE(v_handle);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
v_released := DBMS_LOCK.RELEASE(v_handle);
RAISE;
END;
ELSIF v_result = 1 THEN
DBMS_OUTPUT.PUT_LINE('他セッションが実行中のためスキップ');
ELSE
RAISE_APPLICATION_ERROR(-20001, 'DBMS_LOCK.REQUEST failed: ' || v_result);
END IF;
END;
/
release_on_commit => FALSE は明示解放が必要です。セッションが切れれば解放されますが、それを前提にした設計は避けます。正常系・例外系の両方で RELEASE を呼び、戻り値もログに残しましょう。REQUESTとRELEASEの戻り値を判定する
REQUEST の戻り値を 0 だけ見て終わらせると、タイムアウト、デッドロック、パラメータ不正の区別ができません。運用ログでは、少なくとも成功・タイムアウト・デッドロック・その他を分けます。
CASE v_result
WHEN 0 THEN
DBMS_OUTPUT.PUT_LINE('LOCK_ACQUIRED');
WHEN 1 THEN
DBMS_OUTPUT.PUT_LINE('LOCK_TIMEOUT');
WHEN 2 THEN
DBMS_OUTPUT.PUT_LINE('LOCK_DEADLOCK');
WHEN 3 THEN
DBMS_OUTPUT.PUT_LINE('LOCK_PARAMETER_ERROR');
ELSE
DBMS_OUTPUT.PUT_LINE('LOCK_ERROR=' || v_result);
END CASE;
デッドロックの調査や予防は ORA-00060の原因と解決方法、ロック待ちやNOWAIT系のエラーは ORA-00054完全ガイド も参考になります。
共通パッケージに集約する
DBMS_LOCKを各バッチに直接書くと、タイムアウトや解放漏れの扱いがばらつきます。実務では、ロック取得・解放・戻り値判定を共通パッケージに寄せると安全です。
CREATE OR REPLACE PACKAGE app_lock AS
FUNCTION acquire(
p_lock_name VARCHAR2,
p_timeout NUMBER DEFAULT 0
) RETURN VARCHAR2;
PROCEDURE release(
p_lock_handle VARCHAR2
);
END app_lock;
/
CREATE OR REPLACE PACKAGE BODY app_lock AS
FUNCTION acquire(
p_lock_name VARCHAR2,
p_timeout NUMBER DEFAULT 0
) RETURN VARCHAR2 IS
v_handle VARCHAR2(128);
v_result PLS_INTEGER;
BEGIN
DBMS_LOCK.ALLOCATE_UNIQUE(p_lock_name, v_handle);
v_result := DBMS_LOCK.REQUEST(
lockhandle => v_handle,
lockmode => DBMS_LOCK.X_MODE,
timeout => p_timeout,
release_on_commit => FALSE
);
IF v_result = 0 THEN
RETURN v_handle;
ELSIF v_result = 1 THEN
RETURN NULL;
ELSE
RAISE_APPLICATION_ERROR(-20001, 'lock request failed: ' || v_result);
END IF;
END;
PROCEDURE release(p_lock_handle VARCHAR2) IS
v_result PLS_INTEGER;
BEGIN
IF p_lock_handle IS NOT NULL THEN
v_result := DBMS_LOCK.RELEASE(p_lock_handle);
IF v_result <> 0 THEN
RAISE_APPLICATION_ERROR(-20002, 'lock release failed: ' || v_result);
END IF;
END IF;
END;
END app_lock;
/
この形なら、呼び出し側はロックハンドルを保持し、最後に明示解放するだけで済みます。途中COMMITを含むバッチでもロック寿命を読みやすく保てます。
呼び出し側の実装例
共通パッケージを使う側では、取得できなかった場合にスキップし、取得できた場合だけ本処理を実行します。例外時も finally 相当の位置で解放処理を呼びます。
DECLARE
v_handle VARCHAR2(128);
BEGIN
v_handle := app_lock.acquire('APP:BATCH:DAILY_EXPORT', 0);
IF v_handle IS NULL THEN
DBMS_OUTPUT.PUT_LINE('already running. skip.');
RETURN;
END IF;
BEGIN
-- 本処理。途中COMMITしてもロックは保持される
NULL;
COMMIT;
app_lock.release(v_handle);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
app_lock.release(v_handle);
RAISE;
END;
END;
/
実行ログを残す
二重起動防止は、取得できたかどうかだけでなく、なぜスキップしたのか、何秒処理したのかを追えるようにしておくと運用が楽になります。DBMS_SCHEDULERと組み合わせる場合も、DB標準の実行履歴とは別に業務ログを残すと調査しやすくなります。
CREATE TABLE app_lock_run_log (
log_id NUMBER GENERATED BY DEFAULT AS IDENTITY,
lock_name VARCHAR2(200) NOT NULL,
run_status VARCHAR2(30) NOT NULL,
result_code NUMBER,
started_at TIMESTAMP DEFAULT SYSTIMESTAMP,
finished_at TIMESTAMP,
message VARCHAR2(1000),
CONSTRAINT pk_app_lock_run_log PRIMARY KEY (log_id)
);
-- 例: ロック取得できずスキップした記録
INSERT INTO app_lock_run_log(lock_name, run_status, result_code, message)
VALUES ('APP:BATCH:DAILY_EXPORT', 'SKIPPED', 1, 'already running');
ジョブ履歴や異常検知まで管理する場合は DBMS_SCHEDULER完全ガイド、PL/SQL側のジョブ履歴管理は ジョブ実行履歴の集中管理と異常検知 が近いテーマです。
DBMS_SCHEDULERと組み合わせる
DBMS_SCHEDULER にもジョブの多重実行を制御する考え方はありますが、複数ジョブが同じ外部リソースを使う、手動実行と定期実行がぶつかる、別名ジョブ同士を同じグループで排他したい、という場合はDBMS_LOCKが便利です。
CREATE OR REPLACE PROCEDURE run_daily_export IS
v_handle VARCHAR2(128);
BEGIN
v_handle := app_lock.acquire('APP:RESOURCE:EXPORT_FILE', 0);
IF v_handle IS NULL THEN
-- 既に別ジョブが同じファイル出力を実行中
RETURN;
END IF;
BEGIN
-- ファイル出力や外部連携
NULL;
COMMIT;
app_lock.release(v_handle);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
app_lock.release(v_handle);
RAISE;
END;
END;
/
DBMS_SCHEDULERの高度な運用やCHAIN連携は DBMS_SCHEDULERでジョブ管理を極める も参考になります。
DBMS_ALERTやDBMS_PIPEとの違い
DBMS_LOCK は排他制御のための仕組みであり、通知やメッセージ配送の仕組みではありません。「処理してよいか」を守るならDBMS_LOCK、「処理対象ができたことを知らせる」ならDBMS_ALERTやDBMS_PIPEが近いです。通知と排他を組み合わせる場合は、イベントテーブルに処理対象を保存し、ワーカー起動はALERT、二重処理防止はDBMS_LOCK、という分担にします。
通知系の実装は DBMS_ALERTとDBMS_PIPEの使い方 にまとめています。
共有ロックとCONVERTの使いどころ
読み取り系を複数並行させ、更新系だけ排他にしたい場合は S_MODE と X_MODE を使い分けます。既に取得したロックのモードを変えるには CONVERT を使います。ただし、使い方を誤ると待ちやデッドロックを増やすため、まずは単純なX_MODE排他から始めるのが無難です。
-- 読み取り側 v_result := DBMS_LOCK.REQUEST( lockhandle => v_handle, lockmode => DBMS_LOCK.S_MODE, timeout => 10, release_on_commit => FALSE ); -- 更新前に排他へ変換する例 v_result := DBMS_LOCK.CONVERT( lockhandle => v_handle, lockmode => DBMS_LOCK.X_MODE, timeout => 10 );
デッドロックを避ける設計
複数のロックを取得する場合は、全処理で取得順序を統一します。たとえば顧客単位ロックと月次処理ロックを両方取るなら、必ず月次処理ロックを先に取る、という規則にします。規則がないと、セッションAは顧客ロックを持って月次ロック待ち、セッションBは月次ロックを持って顧客ロック待ち、という循環待ちが起きます。
DBMS_LOCKで守らないほうがよいもの
DBMS_LOCKは便利ですが、データ整合性の最後の砦にするべきではありません。重複登録を防ぐなら一意制約、行更新の衝突を防ぐなら SELECT FOR UPDATE、楽観ロックなら更新日時やバージョン列を使います。DBMS_LOCKは、それらでは表現しづらい「処理単位」「資源単位」の排他に使うときに力を発揮します。
-- 行そのものを更新するなら SELECT FOR UPDATE が自然 SELECT status INTO v_status FROM orders WHERE order_id = p_order_id FOR UPDATE; UPDATE orders SET status = 'PROCESSING' WHERE order_id = p_order_id;
よくある質問
まとめ
DBMS_LOCK は、PL/SQLバッチやジョブの二重起動防止に使いやすいユーザー定義ロックです。ALLOCATE_UNIQUE で安定したロック名からハンドルを取得し、REQUEST で排他ロックを取り、処理後に RELEASE で解放します。
実務で特に重要なのは、途中COMMITがある処理では release_on_commit => TRUE を安易に使わないことです。長時間バッチや複数COMMITを含む処理では、release_on_commit => FALSE と明示RELEASEを基本にし、戻り値、処理時間、スキップ理由をログに残しましょう。行ロックや制約、DBMS_SCHEDULER、DBMS_ALERTと役割分担すれば、PL/SQLだけでもかなり堅牢な同時実行制御を組めます。
参考: Oracle Database PL/SQL Packages and Types Reference – DBMS_LOCK

