當業務交易(business transaction)跨越多個系統交易(system transaction)時,我們無法僅靠資料庫的交易管理來確保資料一致性。本章介紹四種離線並行模式,用以處理這類跨交易的並行控制問題。
Optimistic Offline Lock#
意圖#
透過在提交時偵測衝突來防止並行業務交易之間的資料不一致,若偵測到衝突則回滾交易。
運作方式#
Optimistic Offline Lock 的核心機制是在每筆記錄上維護一個版本號碼(version number)。當 session 載入記錄時,同時記住該版本號碼。提交變更時,將 session 持有的版本與資料庫中的當前版本進行比對:
- 若版本一致,表示沒有其他 session 修改過該記錄,允許提交並遞增版本號碼
- 若版本不一致,表示有其他 session 已修改過,必須回滾交易
在 RDBMS 中,最常見的實作方式是將版本號碼加入 UPDATE 和 DELETE 語句的 WHERE 條件中:
- SQL 執行後檢查 row count:1 表示成功,0 表示衝突
- 一條 SQL 語句即可同時取得鎖定並更新記錄

Figure 16.1: UPDATE optimistic check
除了版本號碼,也應記錄最後修改者與修改時間,以便在通知使用者衝突時提供有意義的訊息。不建議使用時間戳記替代版本號碼,因為系統時鐘不夠可靠,特別是在多伺服器環境下。
不一致讀取問題#
僅在 UPDATE 和 DELETE 時檢查版本是不夠的,還需要處理不一致讀取(inconsistent read)。例如:一個 session 讀取客戶地址來計算稅率,同時另一個 session 修改了該地址。解決方法包括:
- 將讀取的記錄也加入版本檢查的變更集合中
- 使用 Coarse-Grained Lock 將相關物件視為單一可鎖定項目
- 在 Unit of Work 的 commit 中加入
checkConsistentReads()來驗證讀取過的記錄
使用時機#
- 適用於並行 session 之間衝突機率低的情境
- 比 Pessimistic Offline Lock 容易實作,且不容易產生相同的缺陷和執行錯誤
- 應作為業務交易衝突管理的預設方法
- 若衝突頻繁發生,使用者會因為在最後一刻才發現衝突而感到沮喪,此時應改用 Pessimistic Offline Lock
與其問「何時該用樂觀策略?」不如問「何時樂觀策略不夠用?」——樂觀版本可作為悲觀版本的良好補充,正確的並行管理方法是在最大化並行存取的同時最小化衝突。
Pessimistic Offline Lock#
意圖#
透過在存取資料前取得鎖定,確保同一時間只有一個業務交易可以存取某筆資料,從而避免並行衝突。
運作方式#
實作 Pessimistic Offline Lock 需要三個階段:決定鎖定類型、建立 Lock Manager、定義業務交易使用鎖定的規程。
三種鎖定類型#
- Exclusive Write Lock(獨佔寫入鎖):只有要編輯資料的交易需要取得鎖定;不限制讀取,但可能讀到過期資料
- Exclusive Read Lock(獨佔讀取鎖):讀取資料也需要取得鎖定;嚴格確保資料最新,但嚴重限制並行性
- Read/Write Lock(讀寫鎖):結合前兩者優點——讀鎖和寫鎖互斥,但多個讀鎖可以並存
Lock Manager#
Lock Manager 的工作是根據業務交易的請求授予或拒絕鎖定。它需要知道:
- 什麼被鎖定——通常是物件的 ID 或主鍵
- 誰擁有鎖定——業務交易或 session 的識別碼
Lock Manager 的實作可以是記憶體中的 hash table,也可以是資料庫表。若應用伺服器是叢集環境,必須使用資料庫型的 Lock Manager。
鎖定規程#
- 何時鎖定:通常在載入資料之前取得鎖定,以確保拿到的是最新版本
- 何時釋放:在業務交易完成時釋放所有鎖定
- 無法取得鎖定時:最簡單的做法是直接中止交易;應盡早取得鎖定,讓使用者在開始工作前就知道衝突
死鎖(deadlock) 在悲觀鎖定中是真實的威脅。由於業務交易跨越多個系統交易,等待鎖定釋放並不合理。最好的做法是讓 Lock Manager 在鎖定不可用時立即拋出例外,而非等待。
鎖定逾時#
必須管理遺失 session 的鎖定逾時。當客戶端當機或使用者放棄 session 時,擁有的鎖定必須被釋放。常見做法包括:
- 利用 HTTP session 的綁定事件,在 session 過期時釋放鎖定
- 在每個鎖定上記錄時間戳記,超過一定時間視為無效
使用時機#
- 適用於並行 session 之間衝突機率高的情境
- 適用於衝突的代價太高、使用者不能承受工作被白費的情境
- Pessimistic Offline Lock 與 Optimistic Offline Lock 是互補的——應配合使用,只在真正需要的地方使用悲觀鎖定
- 若業務交易可以在單一系統交易內完成,則不需要這些離線鎖定技術
Coarse-Grained Lock#
意圖#
用一個鎖定涵蓋一組相關物件,簡化鎖定管理。
運作方式#
物件經常以群組的方式被編輯。例如一個客戶(Customer)和其地址(Address)集合。如果對每個物件分別鎖定,會帶來以下問題:
- 需要找到群組中的所有成員才能逐一鎖定
- 群組結構複雜時,鎖定管理變得困難
- 大量的鎖定記錄造成鎖定表的競爭和效能問題
Coarse-Grained Lock 透過建立單一競爭點來鎖定整個群組,有兩種主要實作方式:
共享版本(Shared Version)#
群組中的每個物件指向同一個 Version 物件(注意是「共享同一實例」而非「相等的值」)。遞增此共享版本就能鎖定整個群組。這是配合 Optimistic Offline Lock 最自然的方式。

Figure 16.2: Sharing a version

Figure 16.3: Locking a shared version
根鎖定(Root Lock)#
以 Eric Evans 的 Aggregate 概念為基礎——每個 aggregate 有一個 root,鎖定 root 就等於鎖定整個 aggregate 的所有成員。這需要實作從任何群組成員到 root 的導航機制(直接引用或逐層向上)。

Figure 16.4: Locking the root
兩種實作各有取捨。使用關聯式資料庫時,共享版本意味著幾乎所有 SELECT 都需要 join 版本表。根鎖定則需要載入物件並導航到 root,也可能有效能影響。
使用時機#
- 當業務需求要求鎖定整個 aggregate 時(例如編輯一個租約的任一資產,整個租約都應被鎖定)
- 當你希望降低取得和釋放鎖定的成本時
- 可以超出 aggregate 的範圍使用,但要小心不要為了效能而建立不自然的物件關係
Implicit Lock#
意圖#
讓框架或 Layer Supertype 自動取得離線鎖定,避免開發者因疏忽而破壞鎖定機制。
運作方式#
任何鎖定機制的關鍵在於不能有遺漏。忘記寫一行取得鎖定的程式碼,就可能讓整個離線鎖定方案失效。Implicit Lock 的核心思想是將不可遺漏的鎖定任務從開發者手中移交給框架。
實作方式是列出業務交易中鎖定策略所需的所有必要任務,然後確保這些任務由框架(包括 Layer Supertype、框架類別、「管道程式碼」)自動執行:
- 對於 Optimistic Offline Lock:包括儲存版本號碼、在 UPDATE 的 SQL 條件中加入版本、遞增版本等
- 對於 Pessimistic Offline Lock:包括在載入資料時取得鎖定、在 session 結束時釋放所有鎖定等
一個常見的實作技巧是使用 Decorator 模式包裝 Mapper。例如建立一個 LockingMapper,在 find() 方法中自動取得鎖定,然後再委託給真正的 Mapper 執行查詢。透過 Registry 註冊這些 LockingMapper,業務交易在呼叫 mapper 時完全不知道鎖定正在發生。

Figure 16.5: Locking mapper
Implicit Lock 讓開發者可以忽略大部分鎖定機制,但不能忽略後果。例如使用悲觀鎖定時,開發者仍然需要考慮死鎖的可能性。一旦開發者完全停止思考鎖定,業務交易可能以意想不到的方式失敗。
使用時機#
- 幾乎所有有框架概念的應用程式都應該使用 Implicit Lock
- 忘記一個鎖定的風險太大,不應該依賴開發者記得每次都正確地取得和釋放鎖定