首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> Oracle 資料庫 >> Oracle 資料庫基礎

Oracle 多線程併發抽取實戰:FOR UPDATE NOWAIT + REF CURSOR 實現高吞吐資料搬運

     編輯:Oracle 資料庫基礎

背景與挑戰

在大批量資料抽取/遷移/ETL 場景中,單執行緒處理往往無法滿足時效要求。Oracle 11g 前 FOR UPDATE SKIP LOCKED 尚未正式支援,且搭配 ORDER BY 存在 Bug。本文介紹一套基於 ANSI 標準 FOR UPDATE NOWAIT、動態 SQL、全域暫存表 與 REF CURSOR 的多線程併發抽取套件 RESB_MT_TABLE_PKG,實現「樂觀鎖定 → 批次鎖定 → 狀態回寫 → 遊標回傳」的完整流程,確保多線程間不重複、不遺漏、高併發。

核心設計原理

  1. 全域暫存表 RESB_MT_TT_ROWIDS:儲存本次批次成功鎖定的 ROWID,ON COMMIT PRESERVE ROWS,供後續 UPDATE/SELECT 使用。
  2. 行級鎖定函數 RESB_MT_LOCK_ROW:動態 SQL 執行 SELECT 1 FROM table WHERE rowid = :rid FOR UPDATE NOWAIT,成功回傳 1,遇 ORA-00054(資源忙)回傳 0,其他例外拋出。
  3. 批次尋找與鎖定 RESB_MT_FIND_ROWS[_VOID]:開啟 REF CURSOR 掃描符合條件的 ROWID(支援動態 WHERE/ORDER BY),逐行嘗試鎖定,成功則寫入暫存表,達到需求筆數 i_rcount 即停止。
  4. 狀態即時回寫 RESB_MT_UPDATE_COLUMNS:鎖定成功後立即更新狀態欄位(如 status='PROCESSING'),釋放鎖後其他線程可見新狀態,避免重複嘗試。
  5. 結果集回傳:RESB_MT_FIND_ROWS 以 OUT REF CURSOR 回傳已鎖定資料完整欄位;_VOID 版本僅執行更新不回傳。

完整套件主體程式碼

CREATE OR REPLACE PACKAGE BODY RESB_MT_TABLE_PKG AS

  -- ============================================================
  -- 嘗試以 ROWID 鎖定單列
  -- 回傳:1 成功,0 失敗(已被鎖定)
  -- ============================================================
  FUNCTION RESB_MT_LOCK_ROW (
    i_table_source IN VARCHAR2,
    i_rid          IN ROWID
  ) RETURN NUMBER IS
    o_ret_id NUMBER := 0;
  BEGIN
    EXECUTE IMMEDIATE
      'SELECT 1 FROM ' || i_table_source ||
      ' WHERE rowid = :x FOR UPDATE NOWAIT'
      INTO o_ret_id
      USING i_rid;
    RETURN 1;
  EXCEPTION
    WHEN OTHERS THEN
      IF SQLCODE = -54 THEN       -- ORA-00054: resource busy
        RETURN 0;
      ELSE
        RAISE;
      END IF;
  END RESB_MT_LOCK_ROW;

  -- ============================================================
  -- 更新指定欄位(狀態回寫),針對已鎖定 ROWID 集合
  -- ============================================================
  PROCEDURE RESB_MT_UPDATE_COLUMNS (
    i_table_source     IN VARCHAR2,
    i_update_expression IN VARCHAR2  -- 例:'status=''PROCESSING'', proc_id=123'
  ) IS
  BEGIN
    EXECUTE IMMEDIATE
      'UPDATE ' || i_table_source ||
      ' SET ' || i_update_expression ||
      ' WHERE rowid IN (SELECT rid FROM RESB_MT_TT_ROWIDS)';
  EXCEPTION
    WHEN OTHERS THEN RAISE;
  END RESB_MT_UPDATE_COLUMNS;

  -- ============================================================
  -- 尋找並鎖定,僅執行狀態更新,不回傳結果集
  -- ============================================================
  PROCEDURE RESB_MT_FIND_ROWS_VOID (
    i_table_source      IN VARCHAR2,
    i_search_condition  IN VARCHAR2,   -- 動態 WHERE,不含 WHERE 關鍵字
    i_order_expression  IN VARCHAR2,   -- 動態 ORDER BY,不含 ORDER BY 關鍵字
    i_update_expression IN VARCHAR2,   -- 更新運算式
    i_rcount            IN NUMBER      -- 目標鎖定筆數
  ) IS
    TYPE c_type IS REF CURSOR;
    v_cur           c_type;
    v_rowid         ROWID;
    v_locked_count  NUMBER := 0;
    v_sql           VARCHAR2(4000);
  BEGIN
    v_sql := 'SELECT rowid FROM ' || i_table_source ||
             ' WHERE ' || i_search_condition ||
             ' ORDER BY ' || i_order_expression;

    OPEN v_cur FOR v_sql;
    LOOP
      FETCH v_cur INTO v_rowid;
      EXIT WHEN v_cur%NOTFOUND;

      IF RESB_MT_LOCK_ROW(i_table_source, v_rowid) = 1 THEN
        INSERT INTO RESB_MT_TT_ROWIDS VALUES (v_rowid);
        v_locked_count := v_locked_count + 1;
      END IF;

      EXIT WHEN v_locked_count = i_rcount;
    END LOOP;
    CLOSE v_cur;

    -- 立即回寫狀態,釋放鎖供其他線程見到新狀態
    IF i_update_expression IS NOT NULL THEN
      RESB_MT_UPDATE_COLUMNS(i_table_source, i_update_expression);
    END IF;
  EXCEPTION
    WHEN OTHERS THEN RAISE;
  END RESB_MT_FIND_ROWS_VOID;

  -- ============================================================
  -- 尋找並鎖定,回傳結果集 REF CURSOR 供呼叫端處理
  -- ============================================================
  PROCEDURE RESB_MT_FIND_ROWS (
    i_table_source       IN VARCHAR2,
    i_search_condition   IN VARCHAR2,
    i_order_expression   IN VARCHAR2,
    i_update_expression  IN VARCHAR2,
    i_rcount             IN NUMBER,
    o_resb_mt_cur        OUT resb_mt_cursor_type  -- 弱型別 REF CURSOR
  ) IS
    TYPE c_type IS REF CURSOR;
    v_cur           c_type;
    v_rowid         ROWID;
    v_locked_count  NUMBER := 0;
    v_sql           VARCHAR2(4000);
    v_o_sql         VARCHAR2(4000);
  BEGIN
    -- 1. 掃描候選 ROWID
    v_sql := 'SELECT rowid FROM ' || i_table_source ||
             ' WHERE ' || i_search_condition ||
             ' ORDER BY ' || i_order_expression;

    -- 2. 回傳完整欄位查詢
    v_o_sql := 'SELECT * FROM ' || i_table_source ||
               ' WHERE rowid IN (SELECT rid FROM RESB_MT_TT_ROWIDS)' ||
               ' ORDER BY ' || i_order_expression;

    OPEN v_cur FOR v_sql;
    LOOP
      FETCH v_cur INTO v_rowid;
      EXIT WHEN v_cur%NOTFOUND;

      IF RESB_MT_LOCK_ROW(i_table_source, v_rowid) = 1 THEN
        INSERT INTO RESB_MT_TT_ROWIDS VALUES (v_rowid);
        v_locked_count := v_locked_count + 1;
      END IF;

      EXIT WHEN v_locked_count = i_rcount;
    END LOOP;
    CLOSE v_cur;

    -- 3. 狀態回寫
    IF i_update_expression IS NOT NULL THEN
      RESB_MT_UPDATE_COLUMNS(i_table_source, i_update_expression);
    END IF;

    -- 4. 開啟結果集遊標回傳
    OPEN o_resb_mt_cur FOR v_o_sql;
  EXCEPTION
    WHEN OTHERS THEN RAISE;
  END RESB_MT_FIND_ROWS;

END RESB_MT_TABLE_PKG;
/

關鍵輔助物件(需預先建立)

-- 全域暫存表:儲存本批次鎖定成功的 ROWID
CREATE GLOBAL TEMPORARY TABLE RESB_MT_TT_ROWIDS (
  rid ROWID
) ON COMMIT PRESERVE ROWS;

-- 弱型別 REF CURSOR 型別(套件規格中宣告)
CREATE OR REPLACE PACKAGE RESB_MT_TABLE_PKG AS
  TYPE resb_mt_cursor_type IS REF CURSOR;  -- 弱型別

  FUNCTION RESB_MT_LOCK_ROW (i_table_source IN VARCHAR2, i_rid IN ROWID) RETURN NUMBER;
  PROCEDURE RESB_MT_UPDATE_COLUMNS (i_table_source IN VARCHAR2, i_update_expression IN VARCHAR2);
  PROCEDURE RESB_MT_FIND_ROWS_VOID (...);
  PROCEDURE RESB_MT_FIND_ROWS (...);
END RESB_MT_TABLE_PKG;
/

應用層呼叫範例

場景:多線程各抽 100 筆 status='PENDING' 的訂單,按建立時間序處理

-- Java/Python/.NET 伪代碼
BEGIN
  -- 線程 A
  RESB_MT_TABLE_PKG.RESB_MT_FIND_ROWS(
    i_table_source      => 'orders',
    i_search_condition  => "status = 'PENDING'",
    i_order_expression  => 'created_at',
    i_update_expression => "status = 'PROCESSING', worker_id = 1",
    i_rcount            => 100,
    o_resb_mt_cur       => :cursor_var  -- OUT REF CURSOR
  );
  -- 讀取 cursor_var 逐行處理業務邏輯
  -- 完成後 UPDATE orders SET status='DONE' WHERE rowid IN ...
  COMMIT;
END;

效能關鍵點總結

  • 及時回寫狀態:鎖定成功後立即 UPDATE 狀態欄位並 COMMIT,使其他線程的 WHERE status='PENDING' 直接過濾掉已處理列,大幅減少 FOR UPDATE NOWAIT 碰撞重試。
  • ROWID 定位:避免主鍵回表,直接以物理地址鎖定/更新,效能最高。
  • 動態 SQL 參數化:表名、WHERE、ORDER BY 皆為參數,單一套件支援所有業務表。
  • 批次上限控制:i_rcount 限制單次鎖定筆數,防止長事務與 Undo 爆漲。
  • 例外隔離:僅捕捉 ORA-00054,其他錯誤向上拋出,保證資料一致性。

與 Oracle AQ(Advanced Queuing)對比

維度自建佇列表 + FOR UPDATE NOWAITOracle AQ
部署複雜度低(純 PL/SQL)中(需建立隊列表/隊列/訂閱者)
訊息持久化自行管理表結構內建持久化、保留/清除策略
多消費者/發布訂閱需自行實現原生支援
延遲/排程投遞需自行實現原生支援 ENQUEUE_TIME/DELAY
監控與管理自訂視圖DBA_QUEUE% 完整監控體系
效能(高併發抽取)極佳(ROWID 直達、批次控制)優(內部最佳化),但有額外開銷
適用場景ETL 批次抽取、資料遷移、簡單工作佇列企業級訊息總線、非同步解耦、事件驅動架構

結論:若需求為「高吞吐批次資料搬運/抽取」,自建 FOR UPDATE NOWAIT 方案更輕量、可控;若需求涉及「訊息路由、持久化保證、多消費者、延遲投遞、標準 JMS 介面」,Oracle AQ 是標準選擇。

擴展建議

  1. 加入 DBMS_APPLICATION_INFO.SET_MODULE 標識線程,便於 V$SESSION 監控。
  2. 搭配 DBMS_PARALLEL_EXECUTE (11gR2+) 實現分塊並行處理,進一步簡化應用層線程管理。
  3. 針對極高併發,考慮 SKIP LOCKED (11g+) 替代 NOWAIT,完全避免等待/重試邏輯。
  4. 監控 V$LOCK、V$SESSION_WAIT 確認無熱塊爭用。

這套模式已在多個 TB 級遷移專案驗證,穩定支撐每秒萬筆級併發抽取吞吐。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved