首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 悲觀鎖與樂觀鎖實現原理與 Hibernate 實戰配置

     編輯:Java 相關知識

併發控制是企業級應用核心課題。悲觀鎖依賴數據庫鎖機制強勢獨佔,樂觀鎖基於版本號無鎖並發。本文深度解析兩種鎖原理、Hibernate 實現方式、適用場景與代碼級配置指南。

一、悲觀鎖

1. 核心思想

假設併發衝突**一定會發生**,操作前先獲取鎖,持有鎖期間阻塞其他事務訪問同一數據。依賴數據庫排他鎖(X Lock)實現,只有數據庫層鎖才能保證真正排他性(防外部系統繞過應用層修改)。

2. 數據庫原生 SQL

-- MySQL/PostgreSQL/Oracle
SELECT * FROM account WHERE name = 'Erica' FOR UPDATE;
-- 當前事務提交/回滾前,其他事務 SELECT ... FOR UPDATE / UPDATE/DELETE 該行會阻塞

3. Hibernate 實現

// 方式 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);

4. LockMode 枚舉

模式含義典型場景
NONE無鎖默認
WRITE內部用於 Insert/UpdateHibernate 內部
READ內部用於讀取Hibernate 內部
UPGRADESELECT ... FOR UPDATE應用層悲觀鎖首選
UPGRADE_NOWAITOracle FOR UPDATE NOWAIT不等待,立即報錯

5. 關鍵限制

  • **必須在查詢執行前設置 LockMode**,否則 SQL 已生成無 FOR UPDATE 子句,鎖無效。
  • 長事務持有悲觀鎖會嚴重降低併發吞吐,甚至死鎖。
  • 僅對同一數據庫事務內有效,跨事務/外部系統無約束力。

二、樂觀鎖

1. 核心思想

假設併發衝突**較少發生**,不加鎖讀取數據,更新時通過版本號或時間戳檢測衝突:UPDATE ... SET col=val, version=version+1 WHERE id=? AND version=oldVersion。若影響行數為 0 則衝突,拋出異常重試。

2. 業務流程示例

  1. 操作員 A 讀取賬戶(version=1, balance=100)。
  2. 操作員 B 讀取同賬戶(version=1, balance=100)。
  3. A 扣款 50,version=2 提交 → 成功,數據庫 version=2。
  4. B 扣款 20,version=2 提交 → WHERE version=1 不匹配,更新 0 行,拋出 StaleObjectStateException。

3. Hibernate 配置(XML 映射)

<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>

4. 實體類定義

public class Account {
    private Long id;
    private Integer version;  // 必須非 null,Hibernate 自動維護
    private BigDecimal balance;
    private String name;
    // getter/setter
}

5. optimistic-lock 可選策略

策略含義適用性
version版本號欄位(推薦)唯一支持脫管對象(離線修改後重新附加)
timestamp時間戳欄位高併發下可能碰撞,不推薦
dirty檢查所有髒字段無版本號legacy表,性能差
all檢查所有字段極慢,不推薦
none無樂觀鎖只讀數據/外部系統維護

6. 脫管對象樂觀鎖優勢

// 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 策略支持脫管對象重新附加時的版本檢查,這是選擇它的核心原因。

三、悲觀鎖 vs 樂觀鎖選型指南

維度悲觀鎖樂觀鎖
併發衝突頻率高低
事務持有時間短長(含用戶思考時間)
性能開銷數據庫鎖開銷大,易死鎖極低(僅版本號比較)
外部系統保護有效(數據庫層)無效(需外部系統配合)
實現複雜度低(聲明式)低(聲明式)
死鎖風險高無
適用場景庫存扣減、金融結算、資源預訂用戶資料編輯、表單提交、長業務流程

四、進階:數據庫層樂觀鎖(存儲過程/觸發器)

為防外部系統繞過應用層,可在數據庫實現版本檢查:

-- 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 則重試/報錯,實現數據庫級樂觀鎖,徹底解決外部系統繞過問題。

五、最佳實踐清單

  1. 默認選樂觀鎖:@Version / <version>,90% 場景足夠。
  2. 高衝突短事務用悲觀鎖:庫存扣減、座位鎖定,配合 UPGRADE_NOWAIT 失敗快速重試。
  3. 版本號類型用 Integer/Long:避免 Timestamp 精度碰撞。
  4. 動態更新:dynamic-update="true" 僅更新修改字段,減少鎖爭用範圍。
  5. 異常處理:捕獲 StaleObjectStateException(Hibernate)或 OptimisticLockException(JPA),業務邏輯重試或提示用戶刷新。
  6. 批量更新慎用樂觀鎖:大量實體逐個版本檢查性能差,考慮悲觀鎖或分批處理。

鎖不是銀彈,而是併發語義的精確表達。理解悲觀鎖「寧可錯殺不可放過」與樂觀鎖「信任但驗證」的哲學差異,結合業務衝突特徵選型,才能構建高併發下穩定可靠的系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved