在企業級應用開發中,併發控制是確保數據一致性的關鍵。Hibernate 作為主流 ORM 框架,提供了悲觀鎖和樂觀鎖兩種截然不同的鎖定策略。本文將系統梳理兩者的核心原理、實現方式、適用場景及代碼實踐,幫助開發者做出最優選擇。
悲觀鎖假設數據隨時可能被其他事務修改,因此在讀取數據時即在數據庫層面加鎖,依賴數據庫的鎖機制(如 SELECT ... FOR UPDATE)保證數據操作的排他性。這種機制雖然強勢保證了一致性,但會導致併發性能下降,尤其在高併發、長事務場景下,資源等待開銷巨大。
基於 JDBC 直接操作時,典型 SQL 如下:
SELECT * FROM account WHERE name = 'Erica' FOR UPDATE;
此語句會鎖定查詢到的行,其他事務對該行的更新操作將被阻塞,直到當前事務提交或回滾釋放鎖。
Hibernate 通過 LockMode 封裝了數據庫鎖行為,核心用法如下:
String hql = "FROM Account WHERE name = :name";
Query query = session.createQuery(hql);
query.setParameter("name", "Erica");
query.setLockMode("account", LockMode.UPGRADE); // 對應 FOR UPDATE
List<Account> list = query.list();
FOR UPDATE 子句加鎖,這是實現悲觀鎖的關鍵。關鍵提示:必須在查詢執行前設置 LockMode(即 Hibernate 生成 SQL 前),否則數據已加載至 Session,數據庫層面的鎖將失效。
樂觀鎖假設併發衝突較少,不在數據庫加鎖,而是通過應用層邏輯實現版本控制。核心思想:讀取數據時獲取版本號,更新時比對版本號,若版本號不匹配則拒絕更新,拋出 StaleObjectStateException。
假設賬戶餘額 1000 元,版本號 5。
客戶端 A 讀取餘額 1000,版本 5。
客戶端 B 同時讀取餘額 1000,版本 5。
A 提取 500 元,更新餘額為 500,版本號遞增為 6,數據庫版本號由 5→6,更新成功。
B 提取 300 元,嘗試更新餘額為 700,但提交的版本號仍為 5,數據庫版本號已為 6,版本號不匹配,更新失敗,拋出異常。B 需重新讀取最新數據(餘額 500,版本 6)再重試業務邏輯。
實體類添加 version 屬性:
public class Account {
private int version;
// getter/setter
public int getVersion() { return version; }
public void setVersion(int version) { this.version = version; }
}
映射文件配置 <version> 標籤:
<hibernate-mapping>
<class name="onlyfun.caterpillar.Account" table="ACCOUNT" optimistic-lock="version">
<id ... />
<version name="version" column="VERSION"/>
...
</class>
</hibernate-mapping>
設置 optimistic-lock="version" 後,Hibernate 會自動在更新 SQL 中帶上版本號條件(WHERE VERSION = ?),並自動遞增版本號。
捕獲 StaleObjectStateException 後,可採取以下策略:
樂觀鎖無法防禦外部系統繞過應用層直接修改數據庫導致的數據不一致。電子商務等強一致性場景,建議結合以下措施:
| 維度 | 悲觀鎖 | 樂觀鎖 |
|---|---|---|
| 併發性能 | 低(阻塞等待) | 高(無鎖,衝突才重試) |
| 一致性保證 | 強(數據庫級) | 最終一致性(應用級) |
| 適用場景 | 高衝突、短事務、強一致性要求 | 低衝突、長事務、高吞吐量 |
| 實現複雜度 | 低(配置 LockMode) | 中(需版本欄位、異常處理) |
實際項目中,樂觀鎖是首選(Hibernate 官方推薦),僅在極高併發衝突或強一致性業務(如扣款、庫存扣減)下輔以悲觀鎖。合理設計版本號策略、優化事務邊界、引入重試機制,能最大化發揮樂觀鎖的併發優勢。