Concurrency Control#
每個 SQL 語句都在資料庫交易的範圍內執行。為了防止衝突,資料庫引擎使用 row-level lock。資料庫的物理鎖可以隱式或顯式取得 – 當一列被更改時,資料庫自動取得 exclusive lock 來防止 write-write 衝突。
- Pessimistic locking 透過預防來處理並行衝突,可能影響效能和可擴展性
- Optimistic locking 透過衝突偵測來處理,在交易提交時才檢查是否有衝突
為了在確保一致性的同時提高交易吞吐量,許多資料存取框架同時提供 optimistic locking 支援。
Hibernate Optimistic Locking#
Optimistic locking 自 Hibernate 1.0 起就已支援,可以隱式或顯式使用。
隱式 Optimistic Locking 機制#
要啟用隱式 optimistic locking,entity 必須提供 @Version 屬性:
@Entity @Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
@Version
private int version;
}
Figure 16.1: Post entity with a single global version
- 每次 UPDATE 時,Hibernate 會在 WHERE 子句中加入
version條件過濾 - 如果版本已被其他交易更改,update count 為 0,Hibernate 拋出
OptimisticLockException - 內部的
StaleStateException會被包裝為 JPA 規範定義的OptimisticLockException
建議使用數值版本(numerical version)而非 timestamp。系統時間不一定單調遞增,且不同資料庫系統的時間精度差異很大(從 nanoseconds 到 seconds)。在分散式系統中,logical clock 永遠優於 physical clock。
解決 Optimistic Locking 衝突#
Optimistic locking 使用衝突偵測(conflict detection)而非預防,類似 MVCC 的做法。它可以防止 lost update 異常,即使在 multi-request workflow 中,只要 entity 狀態在請求間被保留。
然而,optimistic locking 採用 all-or-nothing 策略 – 即使兩個並行交易更新的是不重疊的屬性子集,只要基於相同的 entity version,第二個更新就會失敗。這在高並行寫入場景下可能導致大量交易回滾。

Figure 16.2: Optimistic locking non-overlapping conflict
拆分 Entity#
為了解決非重疊衝突的問題,可以將 entity 拆分為多個 sub-entity,每個擁有獨立的 version:
- 將
Post拆分為Post(title)、PostLikes(likes)、PostViews(views) - 各 sub-entity 透過 bidirectional one-to-one relationship 關聯,共用同一個 primary key
- 更新
title只會影響Post的 version,更新views只影響PostViews的 version

Figure 16.3: Post entity version split
拆分 entity 可以降低 optimistic locking 的 false positive,但會增加讀取操作的複雜度(需要 JOIN 多張表)。Second-level cache 可以緩解這個讀取效能問題。

Figure 16.4: Optimistic locking multiple versions
Versionless Optimistic Locking#
Hibernate 提供不需要 version 欄位的 optimistic locking 機制,透過 @OptimisticLocking annotation 設定。OptimisticLockType 有以下四種值:
- NONE – 停用隱式 optimistic locking
- VERSION – 使用數值版本或 timestamp(預設)
- DIRTY – 只使用目前 Persistence Context 中被修改的屬性作為 WHERE 條件
- ALL – 使用所有 entity 屬性作為 WHERE 條件
OptimisticLockType.ALL和OptimisticLockType.DIRTY都需要搭配@DynamicUpdateannotation,因為 update 語句必須動態生成,將屬性值放入 WHERE 子句。
OptimisticLockType.ALL 適用於無法修改資料庫表結構以新增 version 欄位的場景。但它的行為等同於單一 global version,非重疊屬性的並行更新仍然會衝突。
OptimisticLockType.DIRTY 允許並行使用者更新不同的屬性而不產生衝突。lost update 是以 per-attribute 為基礎來防止的 – 只有當兩個交易更新相同屬性時才會發生衝突。

Figure 16.5: Optimistic locking dirty attributes
OptimisticLockType.DIRTY 的限制#
OptimisticLockType.DIRTY 有一個重要限制:它無法與 Session.update() 配合使用。
- 當使用
Session.update()重新附加(reattach)detached entity 時,Persistence Context 缺少 loaded state 資訊,無法執行 dirty checking - Hibernate 會直接將整個 entity 狀態寫入資料庫,不附帶任何 optimistic locking 過濾條件,可能導致 lost update
- 即使加上
@SelectBeforeUpdate,Hibernate 會先 SELECT 再 UPDATE,但 optimistic locking 過濾條件仍然不會被加入
使用 EntityManager.merge() 時情況較好 – Hibernate 會先載入最新狀態,optimistic locking 是基於新載入的 entity 狀態執行,但只能偵測到 merge 之後的並行衝突,無法偵測到 entity 被 detach 期間的變更。
使用 versionless optimistic locking 搭配 detached entity 時,建議使用 stateful Persistence Context(如
PersistenceContextType.EXTENDED),讓 entity 永不 detach,確保 loading-time state 不會遺失。
顯式 Locking 機制#
JPA 提供 concurrency control API,應用程式開發者可以實作複雜的資料完整性規則。顯式 locking 同時支援 pessimistic 和 optimistic locking。
JPA 定義的 LockModeType 列表:
| Lock Mode Type | 說明 |
|---|---|
NONE | 使用預設的隱式 locking 機制 |
OPTIMISTIC / READ | 在交易提交時發出 version check |
OPTIMISTIC_FORCE_INCREMENT / WRITE | 在提交前遞增 entity version |
PESSIMISTIC_READ | 取得 shared database lock |
PESSIMISTIC_WRITE | 取得 exclusive database lock |
PESSIMISTIC_FORCE_INCREMENT | 取得 exclusive lock 並立即遞增 version |
PESSIMISTIC_READ 與 PESSIMISTIC_WRITE#
PESSIMISTIC_WRITE– 取得 exclusive lock,Hibernate 產生FOR UPDATE子句PESSIMISTIC_READ– 取得 shared lock,Hibernate 產生FOR SHARE子句(PostgreSQL)
不同資料庫的鎖語法各異:
- Oracle – 僅支援 exclusive lock(
FOR UPDATE) - SQL Server – 使用 table hints(
WITH (HOLDLOCK, ROWLOCK)/WITH (UPDLOCK, HOLDLOCK, ROWLOCK)) - PostgreSQL –
FOR SHARE(shared)/FOR UPDATE(exclusive) - MySQL –
LOCK IN SHARE MODE(shared)/FOR UPDATE(exclusive)
如果底層資料庫不支援 shared lock,使用
PESSIMISTIC_READ時會自動取得 exclusive lock。例如在 Oracle 上,PESSIMISTIC_READ會使用FOR UPDATE。

Figure 16.6: Post, PostDetails, and PostComment
Entity 也可以在載入到 Persistence Context 之後再透過 entityManager.lock() 方法鎖定。
Lock Scope#
預設情況下,lock scope 僅限於被鎖定的 entity 本身。但鎖定請求可以透過以下方式傳播到子 entity:
- Entity query – 在 JPQL 查詢上設定
LockModeType,FOR UPDATE會套用到所有被 SELECT 的 result set 記錄 - CascadeType.Lock – 在子關聯上設定,需搭配
CascadeType.ALL - LockRequest.setScope(true) – Hibernate 專用的 scope 屬性
對 managed entity 使用
setScope(true)時,Hibernate 不會自動 cascade lock 請求。建議使用 entity query locking 來精確控制鎖定範圍。鎖定過多資料會影響可擴展性。
Lock Timeout#
取得 row-level lock 時,建議設定 timeout 值:
- Hibernate API –
LockOptions.setTimeOut() - JPA API –
javax.persistence.lock.timeouthint - NO_WAIT – timeout 設為 0,若鎖已被持有則立即失敗(
FOR UPDATE NOWAIT)
部分資料庫(Oracle 10g、PostgreSQL 9.5)支援 SKIP LOCKED 子句,可以跳過已被鎖定的列。這在實作 job queue 機制時特別有用 – 多個 worker 可以同時查詢待處理的記錄,而不會因為鎖衝突而拋出例外。
Hibernate 透過 LockMode.UPGRADE_SKIPLOCKED 支援此功能。

Figure 16.7: Post with PostStatus
LockModeType.OPTIMISTIC#
LockModeType.OPTIMISTIC(別名 READ)確保在交易提交時檢查 entity version,即使 entity 本身未被修改。
這個模式用於確保讀取一致性 – 如果在交易期間其他交易修改了被讀取的 entity,當前交易在提交時會偵測到版本不匹配並拋出 OptimisticLockException。

Figure 16.8: Post and PostComment entities
不一致性風險#
在 Read Committed 隔離等級下,即使使用 OPTIMISTIC lock,仍可能出現不一致的情況。因為 version check 是在交易提交時才執行,而在 check 和 commit 之間可能存在時間窗口,讓其他交易修改 entity。
要完全防止此問題,需要使用 Repeatable Read 或更高的隔離等級,或搭配 OPTIMISTIC_FORCE_INCREMENT。

Figure 16.9: LockModeType.OPTIMISTIC window of opportunity
OPTIMISTIC_FORCE_INCREMENT#
LockModeType.OPTIMISTIC_FORCE_INCREMENT(別名 WRITE)會在交易提交前主動遞增 entity version,即使 entity 本身未被修改。
這個模式適用於需要關聯兩個不同 entity 的場景 – 當子 entity 被修改時,需要觸發父 entity 的 version 遞增。例如:新增一筆 PostComment 時,同時遞增關聯的 Post entity version,確保其他並行交易能偵測到整個 aggregate 的變更。

Figure 16.10: Repository, Commit, and Change

Figure 16.11: LockModeType.OPTIMISTIC_FORCE_INCREMENT
PESSIMISTIC_FORCE_INCREMENT#
LockModeType.PESSIMISTIC_FORCE_INCREMENT 結合了 pessimistic 和 optimistic locking:
- 立即取得 exclusive database lock(
FOR UPDATE) - 立即遞增 entity version(不等到交易提交)

Figure 16.12: Fail fast PESSIMISTIC_FORCE_INCREMENT lock acquisition
這個模式適用於需要同時防止並行存取且保證 optimistic locking 一致性的場景。與 OPTIMISTIC_FORCE_INCREMENT 不同的是,version 遞增在取得鎖時就發生,而非等到 flush 時。
