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

Oracle 悲觀鎖與樂觀鎖完整指南:行級鎖機制、FOR UPDATE NOWAIT、版本戳與時間戳三大實現模式

     編輯:Oracle 資料庫基礎

並發控制兩大流派:悲觀 vs 樂觀

維度悲觀鎖樂觀鎖
核心假設衝突一定會發生,先鎖後改衝突極少發生,改完再檢查
鎖定時機讀取時(SELECT FOR UPDATE)提交時(UPDATE 時檢查版本)
持有連線整個業務流程需保持連線無需長連線,適合斷線架構
衝突處理阻塞等待或立即報錯拋出異常,應用層重試/彙報
適用場景高衝突、短事務、強一致性要求低衝突、長事務、Web/微服務架構

Oracle 悲觀鎖:行級鎖的精細控制

不同於 SQL Server 頁級/表級鎖易造成熱點阻塞,Oracle 採用行級鎖,僅鎖定被訪問的具體資料列,並發效能極佳。

測試環境建立

CREATE TABLE test_lock(
    id NUMBER PRIMARY KEY,
    name VARCHAR2(20),
    location VARCHAR2(20),
    value NUMBER
) AS SELECT deptno, dname, loc, 1 FROM scott.dept;

模式一:SELECT ... FOR UPDATE(預設 WAIT)

-- Session 1:鎖定 id=10
SELECT * FROM test_lock WHERE id = 10 FOR UPDATE;
-- 此時該列被鎖定,Session 1 未 COMMIT/ROLLBACK 前持續持有鎖

-- Session 2:嘗試同樣鎖定
SELECT * FROM test_lock WHERE id = 10 FOR UPDATE;
-- ⏳ 阻塞等待... 直到 Session 1 釋放鎖

-- Session 2:若不加 FOR UPDATE
SELECT * FROM test_lock WHERE id = 10;
-- ✅ 立即返回,不阻塞(讀不阻塞寫、寫不阻塞讀)

模式二:SELECT ... FOR UPDATE NOWAIT(非阻塞)

-- Session 1 持有鎖時,Session 2 執行
SELECT * FROM test_lock WHERE id = 10 FOR UPDATE NOWAIT;
-- ❌ 立即報錯:ORA-00054: resource busy and acquire with NOWAIT specified

-- 應用層捕獲錯誤碼 54,可實現「樂觀重試」或「用戶提示稍後再試」

模式三:FOR UPDATE SKIP LOCKED(11g+,佇列處理神器)

-- 跳過已鎖定列,處理下一筆可用資料
SELECT * FROM task_queue WHERE status='PENDING' FOR UPDATE SKIP LOCKED;
-- 適用於:分散式任務佇列、多工作者消費模式

鎖定後的 DML 行為

-- Session 1:SELECT ... FOR UPDATE 鎖定 id=10
-- Session 2:嘗試更新同一列
UPDATE test_lock SET value = 2 WHERE id = 10;
-- ⏳ 阻塞等待 Session 1 釋放鎖

-- Session 1 COMMIT 後,Session 2 UPDATE 成功並獲得鎖
-- Session 2 未 COMMIT 前,其他 Session 仍無法鎖定/更新該列

關鍵特性:Oracle 悲觀鎖不鎖定整張表,僅鎖定符合 WHERE 條件的列。大量並發插入/更新不同主鍵值時幾乎零衝突。

樂觀鎖三大實現模式

模式一:完整資料比對(最簡單、開銷最大)

  1. 讀取時將整列資料快照保存在應用端(Session/物件)
  2. 更新時 WHERE 條件包含所有原始欄位值
  3. 若 ROW_COUNT = 0 表示資料已被修改,拋出並發異常
-- 讀取
SELECT id, name, location, value FROM test_lock WHERE id = 10;
-- 應用端保存:(10, 'SALES', 'CHICAGO', 1)

-- 更新(比對所有欄位)
UPDATE test_lock 
SET value = 2 
WHERE id = 10 
  AND name = 'SALES' 
  AND location = 'CHICAGO' 
  AND value = 1;

-- 若無列更新 → 並發衝突

模式二:版本戳 / Version Number(Hibernate/JPA 標準)

  1. 表新增 VERSION NUMBER 欄位,預設 1
  2. 每次更新 SET version = version + 1
  3. 更新 WHERE 條件帶入 AND version = :old_version
ALTER TABLE test_lock ADD (version NUMBER DEFAULT 1);

-- 讀取
SELECT id, name, value, version FROM test_lock WHERE id = 10;
-- 取得 version = 5

-- 更新(應用端或資料庫觸發器自動 +1)
UPDATE test_lock 
SET value = 2, version = version + 1 
WHERE id = 10 AND version = 5;

-- 0 列更新 → 衝突,拋出 OptimisticLockException

驗證位置選擇:

  • 應用層驗證(推薦):由 ORM 框架自動處理,效能最佳
  • 資料庫觸發器驗證:BEFORE UPDATE 檢查版本,違規 RAISE_APPLICATION_ERROR。缺點:觸發器開銷大、除錯難、邏輯分散

模式三:時間戳 / Timestamp(Oracle 9i+ 高精度)

-- 使用 TIMESTAMP WITH TIME ZONE(微秒精度,足夠區分並發)
ALTER TABLE test_lock ADD (last_modified TIMESTAMP WITH TIME ZONE);

-- 讀取時取得時間戳
SELECT id, name, value, last_modified FROM test_lock WHERE id = 10;

-- 更新比對時間戳
UPDATE test_lock 
SET value = 2, last_modified = SYSTIMESTAMP 
WHERE id = 10 AND last_modified = :old_timestamp;

精度建議:Oracle TIMESTAMP WITH TIME ZONE 精度達微秒(10-6 秒),配合資料庫處理時間與人工思考延遲,極難發生碰撞。若系統極高併發(單表千 QPS+),建議改用版本號避免理論碰撞風險。

悲觀鎖 vs 樂觀鎖:選型決策樹


開始
  │
  ├─ 衝突頻率高?(同一筆資料秒級多次爭搶)
  │     ├─ 是 → 事務極短?(<100ms)
  │     │     ├─ 是 → 【悲觀鎖 FOR UPDATE NOWAIT】
  │     │     └─ 否 → 【悲觀鎖 + 業務拆分/縮短事務】
  │     └─ 否 → 
  ├─ 架構為斷線/無狀態?(Web、微服務、批次處理)
  │     ├─ 是 → 【樂觀鎖 版本號/時間戳】
  │     └─ 否 → 
  ├─ 業務可接受「最後寫入者勝」?
  │     ├─ 是 → 【無鎖 / 覆蓋寫入】
  │     └─ 否 → 【樂觀鎖 + 衝突解決策略(合併/彙報/重試)】
  └─ 不確定 → 【預設樂觀鎖(版本號),監控衝突率再調整】

實務避坑指南

坑點後果解法
悲觀鎖事務過長連線池耗盡、大面積阻塞縮短事務、拆分業務、用 NOWAIT/SKIP LOCKED
忘記 COMMIT/ROLLBACK鎖永不釋放,資料庫假死連線池設定 Idle Timeout、應用層 try-finally 強制釋放
樂觀鎖版本號未加索引UPDATE WHERE version=? 全表掃描版本號欄位建立索引(通常含在 PK/UK 中)
時間戳精度不足高併發下誤判無衝突用 TIMESTAMP WITH TIME ZONE 或改版本號
觸發器實現樂觀鎖效能差、死鎖風險、難除錯移至應用層/ORM 框架處理

總結

Oracle 的行級悲觀鎖(FOR UPDATE [NOWAIT/SKIP LOCKED])效能極佳,適合高衝突短事務;樂觀鎖(版本號/時間戳)是現代斷線架構的標準選擇,由應用層/ORM 自動管理。**核心原則:預設樂觀鎖,衝突率高且事務短時轉悲觀鎖,嚴禁長事務持有悲觀鎖**。

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