併發控制是企業級應用核心課題。悲觀鎖依賴數據庫鎖機制強勢獨佔,樂觀鎖基於版本號無鎖並發。本文深度解析兩種鎖原理、Hibernate 實現方式、適用場景與代碼級配置指南。
假設併發衝突**一定會發生**,操作前先獲取鎖,持有鎖期間阻塞其他事務訪問同一數據。依賴數據庫排他鎖(X Lock)實現,只有數據庫層鎖才能保證真正排他性(防外部系統繞過應用層修改)。
-- MySQL/PostgreSQL/Oracle
SELECT * FROM account WHERE name = 'Erica' FOR UPDATE;
-- 當前事務提交/回滾前,其他事務 SELECT ... FOR UPDATE / UPDATE/DELETE 該行會阻塞
// 方式 1:Query.setLockMode
Query query = session.createQuery("from Account a where a.name = :name");
query.setParameter("name", "Erica");
query.setLockMode("a", LockMode.UPGRADE); // 生成 FOR UPDATE
List<Account> list = query.list();
// 方式 2:Criteria.setLockMode
Criteria criteria = session.createCriteria(Account.class);
criteria.add(Restrictions.eq("name", "Erica"));
criteria.setLockMode(LockMode.UPGRADE);
// 方式 3:Session.lock (重新附加已加載對象)
session.lock(account, LockMode.UPGRADE);
| 模式 | 含義 | 典型場景 |
|---|---|---|
NONE | 無鎖 | 默認 |
WRITE | 內部用於 Insert/Update | Hibernate 內部 |
READ | 內部用於讀取 | Hibernate 內部 |
UPGRADE | SELECT ... FOR UPDATE | 應用層悲觀鎖首選 |
UPGRADE_NOWAIT | Oracle FOR UPDATE NOWAIT | 不等待,立即報錯 |
假設併發衝突**較少發生**,不加鎖讀取數據,更新時通過版本號或時間戳檢測衝突:UPDATE ... SET col=val, version=version+1 WHERE id=? AND version=oldVersion。若影響行數為 0 則衝突,拋出異常重試。
StaleObjectStateException。<class name="Account" table="account" optimistic-lock="version" dynamic-update="true">
<id name="id" column="id">
<generator class="native"/>
</id>
<version name="version" column="version" type="integer"/>
<!-- 必須在 id 節點之後 -->
<property name="balance" column="balance" type="big_decimal"/>
<property name="name" column="name"/>
</class>
public class Account {
private Long id;
private Integer version; // 必須非 null,Hibernate 自動維護
private BigDecimal balance;
private String name;
// getter/setter
}
| 策略 | 含義 | 適用性 |
|---|---|---|
version | 版本號欄位(推薦) | 唯一支持脫管對象(離線修改後重新附加) |
timestamp | 時間戳欄位 | 高併發下可能碰撞,不推薦 |
dirty | 檢查所有髒字段 | 無版本號legacy表,性能差 |
all | 檢查所有字段 | 極慢,不推薦 |
none | 無樂觀鎖 | 只讀數據/外部系統維護 |
// Web 請求 1:加載對象放入 Session/HTTP Session
Account acc = session.get(Account.class, 1L); // version=5
// 用戶修改表單提交(請求 2,新 Session)
Account detached = (Account) httpSession.get("account"); // version=5
detached.setBalance(newBalance);
// 重新附加並合併
session.lock(detached, LockMode.NONE); // 或 session.merge(detached)
// Hibernate 檢查 version=5 是否匹配數據庫當前版本
session.flush(); // 執行 UPDATE ... WHERE version=5
只有 version 策略支持脫管對象重新附加時的版本檢查,這是選擇它的核心原因。
| 維度 | 悲觀鎖 | 樂觀鎖 |
|---|---|---|
| 併發衝突頻率 | 高 | 低 |
| 事務持有時間 | 短 | 長(含用戶思考時間) |
| 性能開銷 | 數據庫鎖開銷大,易死鎖 | 極低(僅版本號比較) |
| 外部系統保護 | 有效(數據庫層) | 無效(需外部系統配合) |
| 實現複雜度 | 低(聲明式) | 低(聲明式) |
| 死鎖風險 | 高 | 無 |
| 適用場景 | 庫存扣減、金融結算、資源預訂 | 用戶資料編輯、表單提交、長業務流程 |
為防外部系統繞過應用層,可在數據庫實現版本檢查:
-- PostgreSQL 條件更新函數
CREATE OR REPLACE FUNCTION update_account_balance(p_id BIGINT, p_new_balance NUMERIC, p_expected_version INT)
RETURNS BOOLEAN AS $$
BEGIN
UPDATE account
SET balance = p_new_balance, version = version + 1
WHERE id = p_id AND version = p_expected_version;
RETURN FOUND;
END;
$$ LANGUAGE plpgsql;
應用層調用函數,返回 false 則重試/報錯,實現數據庫級樂觀鎖,徹底解決外部系統繞過問題。
@Version / <version>,90% 場景足夠。UPGRADE_NOWAIT 失敗快速重試。dynamic-update="true" 僅更新修改字段,減少鎖爭用範圍。StaleObjectStateException(Hibernate)或 OptimisticLockException(JPA),業務邏輯重試或提示用戶刷新。鎖不是銀彈,而是併發語義的精確表達。理解悲觀鎖「寧可錯殺不可放過」與樂觀鎖「信任但驗證」的哲學差異,結合業務衝突特徵選型,才能構建高併發下穩定可靠的系統。