【Oracle】DBMS_LOCK完全ガイド|ユーザー定義ロックで排他制御・バッチ二重起動防止まで解説

Oracle のテーブルロック・行ロックは DML(INSERT/UPDATE/DELETE)に連動して自動的に取得されますが、「バッチジョブを同時に複数起動させたくない」「特定のリソースへのアクセスをシリアライズしたい」という場面では、テーブルと無関係なアプリケーション専用のロックが必要になります。

DBMS_LOCK パッケージを使うと、任意の名前でロックを確保し、セッション間でそのロックの取得・解放を制御できます。Oracle のトランザクション管理と連携しないため、コミット・ロールバックと独立したロック制御が可能です。

この記事でわかること

  • DBMS_LOCK の仕組みと Oracle の通常ロックとの違い
  • ALLOCATE_UNIQUE でロック ID を確保する方法
  • REQUEST(lock_mode / timeout / release_on_commit)でロックを取得する方法
  • REQUEST の戻り値の意味と判定方法
  • RELEASE でロックを解放する方法
  • バッチジョブの二重起動防止の実装パターン
スポンサーリンク

DBMS_LOCK の仕組みと通常ロックとの違い

項目 テーブルロック・行ロック DBMS_LOCK(ユーザー定義ロック)
取得タイミング DML 実行時に自動取得 DBMS_LOCK.REQUEST を明示的に呼び出す
コミット時の動作 COMMIT でロックが解放される COMMIT でも保持(release_on_commit=FALSE のとき)
対象リソース 行・テーブルに紐付く 任意の名前(文字列)に紐付く
タイムアウト NOWAIT / WAIT n 句で指定 REQUEST の timeout パラメータで秒単位に指定
主な用途 データ整合性の保護 バッチ二重起動防止・非DB リソースの排他制御
必要な権限
DBMS_LOCK パッケージへの EXECUTE 権限が必要です。DBA が GRANT EXECUTE ON DBMS_LOCK TO user_name; で付与してください。Oracle 18c 以降は DBMS_SESSION パッケージでも同等の機能(SESSION_LOCK_WAIT など)が提供されています。

ALLOCATE_UNIQUE でロック ID を確保する

DBMS_LOCK.ALLOCATE_UNIQUE は、ロック名(文字列)をグローバルに一意のロック ID(NUMBER)に変換します。ロック ID を DBMS_LOCK_ALLOCATED テーブルに永続化するため、異なるセッションから同じ名前でロック ID を取得できます。

ALLOCATE_UNIQUE でロック名をロック ID に変換する
DECLARE
    v_lock_id  NUMBER;
BEGIN
    -- ロック名(最大 128 バイト)を一意なロック ID に変換する
    -- 同じ名前を複数セッションから呼び出しても同じ ID が返る
    DBMS_LOCK.ALLOCATE_UNIQUE(
        lockname        => 'BATCH_MONTHLY_SALES',   -- ロック名(任意の文字列)
        lockhandle      => v_lock_id,                  -- 出力: ロックハンドル(文字列)
        expiration_secs => 864000                       -- ロック ID の保持期間(秒)= 10 日間
    );
    -- v_lock_id はこのセッション内で REQUEST / RELEASE に使う

    DBMS_OUTPUT.PUT_LINE('Lock handle: ' || v_lock_id);
END;
/
-- 注意: ALLOCATE_UNIQUE は内部で COMMIT を実行する
-- ALLOCATE_UNIQUE の後に同一トランザクション内の DML を ROLLBACK すると消えてしまうため
-- ALLOCATE_UNIQUE はトランザクション開始前に呼び出すのが安全

REQUEST でロックを取得する

DBMS_LOCK.REQUEST はロックの取得を試みます。戻り値で取得結果を確認し、失敗時は適切に処理します。

REQUEST の戻り値と意味
-- DBMS_LOCK.REQUEST の戻り値
-- 0: 取得成功
-- 1: タイムアウト(timeout 秒以内に取得できなかった)
-- 2: デッドロック検出(Oracle がデッドロックを検知して自動解放)
-- 3: パラメータエラー
-- 4: 既に同じロックを保持している(ALREADY_OWN)
-- 5: 不正なロックハンドル
REQUEST の基本的な使い方とタイムアウト処理
DECLARE
    v_lockhandle  VARCHAR2(128);
    v_result      INTEGER;
BEGIN
    -- ロックハンドルを取得する
    DBMS_LOCK.ALLOCATE_UNIQUE(
        lockname   => 'MY_BATCH_JOB',
        lockhandle => v_lockhandle
    );

    -- 排他ロックを取得する(最大 0 秒待機 = 即時取得を試みる)
    v_result := DBMS_LOCK.REQUEST(
        lockhandle        => v_lockhandle,
        lockmode          => DBMS_LOCK.X_MODE,  -- 排他ロック(Exclusive)
        timeout           => 0,                  -- 0 = 待機なし(NOWAIT 相当)
        release_on_commit => FALSE               -- COMMIT しても解放しない
    );

    IF v_result = 0 THEN
        DBMS_OUTPUT.PUT_LINE('ロック取得成功 - バッチ処理を開始します');
        -- バッチ処理をここに書く

        -- 処理完了後にロックを解放する
        v_result := DBMS_LOCK.RELEASE(v_lockhandle);
        IF v_result = 0 THEN
            DBMS_OUTPUT.PUT_LINE('ロック解放成功');
        END IF;

    ELSIF v_result = 1 THEN
        DBMS_OUTPUT.PUT_LINE('ロック取得タイムアウト - 他のセッションが処理中です');
    ELSIF v_result = 4 THEN
        DBMS_OUTPUT.PUT_LINE('既にロックを保持しています');
    ELSE
        DBMS_OUTPUT.PUT_LINE('ロック取得失敗: ' || v_result);
    END IF;
END;
/
ロックモードの種類
DBMS_LOCK.NL_MODE(1): Null モード – ロックを保持するが他セッションをブロックしない
DBMS_LOCK.SS_MODE(2): Sub-Shared – 読み取り意図を示す
DBMS_LOCK.SX_MODE(3): Sub-Exclusive – 更新意図を示す
DBMS_LOCK.S_MODE(4): Shared – 共有ロック(複数セッションが同時保持可能)
DBMS_LOCK.SSX_MODE(5): Shared Sub-Exclusive
DBMS_LOCK.X_MODE(6): Exclusive – 排他ロック(1 セッションのみ保持可能)
バッチ二重起動防止には X_MODE(排他)を使うのが一般的です。

バッチジョブの二重起動防止の実装

最もよく使われるパターンです。バッチ起動時にロックを取得し、別プロセスが同じバッチを起動しようとするとロックが取れないため即座に終了できます。

バッチ二重起動防止プロシージャ
CREATE OR REPLACE PROCEDURE run_monthly_batch AS
    v_lockhandle  VARCHAR2(128);
    v_result      INTEGER;
BEGIN
    -- ロックハンドルを確保する(バッチ名を使う)
    DBMS_LOCK.ALLOCATE_UNIQUE(
        lockname        => 'BATCH:MONTHLY_SALES_AGGREGATE',
        lockhandle      => v_lockhandle,
        expiration_secs => 86400   -- 1 日
    );

    -- 排他ロックを即時取得する(待機しない)
    v_result := DBMS_LOCK.REQUEST(
        lockhandle        => v_lockhandle,
        lockmode          => DBMS_LOCK.X_MODE,
        timeout           => 0,
        release_on_commit => FALSE  -- COMMIT してもロックを保持し続ける
    );

    IF v_result != 0 THEN
        -- ロック取得失敗 = 別プロセスが実行中
        DBMS_OUTPUT.PUT_LINE(
            '[SKIP] バッチは既に実行中です(result=' || v_result || ')');
        RETURN;   -- 正常終了(エラーにしない)
    END IF;

    -- ロック取得成功 = このセッションだけが処理を実行する
    BEGIN
        DBMS_OUTPUT.PUT_LINE('[START] 月次集計バッチを開始します');

        -- バッチのメイン処理
        INSERT INTO monthly_summary
        SELECT TRUNC(sale_date, 'MM'), SUM(amount)
        FROM sales
        WHERE TRUNC(sale_date, 'MM') = TRUNC(SYSDATE, 'MM')
        GROUP BY TRUNC(sale_date, 'MM');
        COMMIT;

        DBMS_OUTPUT.PUT_LINE('[DONE] 月次集計バッチが完了しました');

    EXCEPTION
        WHEN OTHERS THEN
            ROLLBACK;
            DBMS_OUTPUT.PUT_LINE('[ERROR] ' || SQLERRM);
            -- エラー時も必ずロックを解放する
    END;

    -- 処理完了後にロックを解放する
    v_result := DBMS_LOCK.RELEASE(v_lockhandle);

EXCEPTION
    WHEN OTHERS THEN
        -- 予期しないエラーでもロックを解放する
        IF v_lockhandle IS NOT NULL THEN
            v_result := DBMS_LOCK.RELEASE(v_lockhandle);
        END IF;
        RAISE;
END run_monthly_batch;
/

タイムアウト付きロック待機

即時取得ではなく、一定時間だけ待機してからロックを取得する場合は timeout に秒数を指定します。

タイムアウト付きロック取得(最大 30 秒待機)
DECLARE
    v_lockhandle  VARCHAR2(128);
    v_result      INTEGER;
BEGIN
    DBMS_LOCK.ALLOCATE_UNIQUE(
        lockname   => 'RESOURCE:EXTERNAL_API_SLOT_1',
        lockhandle => v_lockhandle
    );

    -- 最大 30 秒待機してロックを取得する
    v_result := DBMS_LOCK.REQUEST(
        lockhandle        => v_lockhandle,
        lockmode          => DBMS_LOCK.X_MODE,
        timeout           => 30,    -- 30 秒待機
        release_on_commit => FALSE
    );

    CASE v_result
        WHEN 0 THEN
            -- 処理実行
            process_api_call;
            v_result := DBMS_LOCK.RELEASE(v_lockhandle);
        WHEN 1 THEN
            RAISE_APPLICATION_ERROR(-20001, '30秒待機後もロックを取得できませんでした');
        WHEN 2 THEN
            RAISE_APPLICATION_ERROR(-20002, 'デッドロックを検出しました');
        ELSE
            RAISE_APPLICATION_ERROR(-20003, 'ロック取得エラー: ' || v_result);
    END CASE;
END;
/
-- DBMS_LOCK.REQUEST の timeout に DBMS_LOCK.MAXWAIT(32767)を指定すると無限待機になる

V$LOCK でユーザー定義ロックを確認する

V$LOCK でユーザー定義ロック(UL タイプ)を確認する
-- ユーザー定義ロック(type='UL')を確認する
SELECT
    l.sid,
    s.username,
    s.program,
    l.type,         -- 'UL' = User Lock(DBMS_LOCK が作成したロック)
    l.lmode,        -- 現在保持しているロックモード(6=X_MODE)
    l.request,      -- 待機中のロックモード(0=待機なし)
    l.block,        -- 1=他セッションをブロックしている
    l.id1,          -- DBMS_LOCK_ALLOCATED.lockid に対応
    l.id2
FROM V$LOCK l
JOIN V$SESSION s ON l.sid = s.sid
WHERE l.type = 'UL'
ORDER BY l.block DESC, l.sid;

-- DBMS_LOCK_ALLOCATED でロック名と ID の対応を確認する
SELECT lockid, name, expiration
FROM DBMS_LOCK_ALLOCATED
WHERE name LIKE 'BATCH:%'
ORDER BY name;

まとめ

  • DBMS_LOCK:テーブル・行と無関係な任意名称のロックを取得できる。アプリケーション協調制御・バッチ二重起動防止に最適
  • ALLOCATE_UNIQUE:ロック名をグローバルなロックハンドルに変換する。内部で COMMIT が発生するため、トランザクション開始前に呼び出す
  • REQUEST の戻り値:0=成功、1=タイムアウト、2=デッドロック。必ず戻り値を確認して処理する
  • release_on_commit=FALSE:COMMIT してもロックを保持する。バッチ全体をロックしたい場合に指定する
  • RELEASE は必須:セッション終了時に自動解放されるが、プログラム内で明示的に RELEASE することを推奨
  • V$LOCK の type=’UL’:ユーザー定義ロックは UL タイプとして V$LOCK に表示される

セッションのロック状況全般については セッション・ロック完全ガイドを参照してください。デッドロックの検出と解析は デッドロック完全ガイドも参照してください。