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.ALLOptimisticLockType.DIRTY 都需要搭配 @DynamicUpdate annotation,因為 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)
  • PostgreSQLFOR SHARE(shared)/ FOR UPDATE(exclusive)
  • MySQLLOCK 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 查詢上設定 LockModeTypeFOR 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 APILockOptions.setTimeOut()
  • JPA APIjavax.persistence.lock.timeout hint
  • 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 時。

Figure 16.13: LockModeType.PESSIMISTIC_FORCE_INCREMENT