| 維度 | 悲觀鎖 | 樂觀鎖 |
|---|---|---|
| 核心假設 | 衝突一定會發生,先鎖後改 | 衝突極少發生,改完再檢查 |
| 鎖定時機 | 讀取時(SELECT FOR UPDATE) | 提交時(UPDATE 時檢查版本) |
| 持有連線 | 整個業務流程需保持連線 | 無需長連線,適合斷線架構 |
| 衝突處理 | 阻塞等待或立即報錯 | 拋出異常,應用層重試/彙報 |
| 適用場景 | 高衝突、短事務、強一致性要求 | 低衝突、長事務、Web/微服務架構 |
不同於 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;
-- 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;
-- ✅ 立即返回,不阻塞(讀不阻塞寫、寫不阻塞讀)
-- Session 1 持有鎖時,Session 2 執行
SELECT * FROM test_lock WHERE id = 10 FOR UPDATE NOWAIT;
-- ❌ 立即報錯:ORA-00054: resource busy and acquire with NOWAIT specified
-- 應用層捕獲錯誤碼 54,可實現「樂觀重試」或「用戶提示稍後再試」
-- 跳過已鎖定列,處理下一筆可用資料
SELECT * FROM task_queue WHERE status='PENDING' FOR UPDATE SKIP LOCKED;
-- 適用於:分散式任務佇列、多工作者消費模式
-- 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 條件的列。大量並發插入/更新不同主鍵值時幾乎零衝突。
WHERE 條件包含所有原始欄位值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 欄位,預設 1SET version = version + 1WHERE 條件帶入 AND version = :old_versionALTER 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 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+),建議改用版本號避免理論碰撞風險。
開始
│
├─ 衝突頻率高?(同一筆資料秒級多次爭搶)
│ ├─ 是 → 事務極短?(<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 自動管理。**核心原則:預設樂觀鎖,衝突率高且事務短時轉悲觀鎖,嚴禁長事務持有悲觀鎖**。