樂觀鎖和悲觀鎖,如何區(qū)分?
悲觀鎖和樂觀鎖是兩種常見的并發(fā)控制機制,用于處理多線程或多進程環(huán)境中的數(shù)據(jù)訪問沖突問題。它們在數(shù)據(jù)庫系統(tǒng)、分布式系統(tǒng)和多線程編程中都有廣泛應用。這篇文章我們來分析它們的原理以及使用場景。

一、悲觀鎖
1.定義
悲觀鎖(Pessimistic Lock)是一種假設沖突會頻繁發(fā)生的鎖機制。每次數(shù)據(jù)訪問時,都會先加鎖,直到操作完成后才釋放鎖,這樣可以確保在鎖持有期間,其他線程無法訪問這段數(shù)據(jù),從而避免了并發(fā)沖突。
悲觀鎖的實現(xiàn)通常有以下兩種方式:
- 數(shù)據(jù)庫:在數(shù)據(jù)庫中,悲觀鎖通常通過SQL語句實現(xiàn),例如SELECT ... FOR UPDATE。
 - 編程語言:在編程語言中,悲觀鎖可以使用互斥鎖(Mutex)或同步塊(Synchronized Block)來實現(xiàn)。
 
2.應用場景
適用于對數(shù)據(jù)并發(fā)沖突非常敏感的場景,例如銀行轉(zhuǎn)賬操作、庫存扣減等需要嚴格數(shù)據(jù)一致性的操作。
3.優(yōu)缺點
- 優(yōu)點:可以完全避免并發(fā)沖突,保證數(shù)據(jù)的一致性和完整性。
 - 缺點:由于每次訪問數(shù)據(jù)都需要加鎖和解鎖,會導致性能開銷較大,特別是在并發(fā)量高的情況下,容易造成鎖競爭和死鎖問題。
 
4.示例
下面我們用 Java + MySQL 展示了一個悲觀鎖的具體實現(xiàn)。
假設有一個銀行賬戶表(Account),包含賬戶 ID和余額兩個字段,我們希望在更新賬戶余額時使用悲觀鎖,以確保數(shù)據(jù)的一致性。
整個運行流程分為以下4個步驟:
- 獲取賬戶信息并鎖定記錄(SELECT ... FOR UPDATE)。
 - 計算新的余額。
 - 更新賬戶信息。
 - 由于使用了@Transactional注解,整個方法執(zhí)行在一個事務中,確保在事務提交之前,鎖定的記錄不會被其他事務修改。
 
(1) 數(shù)據(jù)庫表結構
CREATE TABLE Account (
    id INT PRIMARY KEY,
    balance DECIMAL(10, 2) NOT NULL
);(2) Java實現(xiàn)示例
Account類:
public class Account {
    private int id;
    private BigDecimal balance;
    // Getters and Setters
}AccountMapper接口:
public interface AccountMapper {
    Account getAccountByIdForUpdate(int id);
    void updateAccount(Account account);
}AccountMapper的SQL實現(xiàn):
<mapper namespace="com.example.AccountMapper">
    <select id="getAccountByIdForUpdate" resultType="com.example.Account">
        SELECT id, balance FROM Account WHERE id = #{id} FOR UPDATE
    </select>
    <update id="updateAccount">
        UPDATE Account
        SET balance = #{balance}
        WHERE id = #{id}
    </update>
</mapper>AccountService類:
import org.springframework.transaction.annotation.Transactional;
public class AccountService {
    private AccountMapper accountMapper;
    public AccountService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }
    @Transactional
    public void updateAccountBalance(int accountId, BigDecimal amount) {
        // 獲取賬戶信息并鎖定記錄
        Account account = accountMapper.getAccountByIdForUpdate(accountId);
        if (account == null) {
            throw new RuntimeException("Account not found");
        }
        // 更新余額
        account.setBalance(account.getBalance().add(amount));
        // 更新賬戶信息
        accountMapper.updateAccount(account);
    }
}示例說明:
- Account類:包含賬戶ID和余額的Java類。
 - AccountMapper接口:定義了獲取賬戶信息(帶鎖定)和更新賬戶信息的方法。
 - AccountMapper的SQL實現(xiàn):使用MyBatis或其他ORM框架,定義了SQL查詢和更新語句。注意在查詢語句中使用FOR UPDATE來鎖定記錄。
 - AccountService類:業(yè)務邏輯類,在更新賬戶余額時,先獲取當前賬戶信息并鎖定記錄,然后更新余額并提交更新。
 
這種機制確保了在操作完成之前,其他線程無法修改鎖定的記錄,從而實現(xiàn)了悲觀鎖的并發(fā)控制。
(3) 注意事項
- 事務管理:使用悲觀鎖時,需要確保在事務提交之前鎖不會被釋放,因此必須在事務中使用。
 - 死鎖風險:悲觀鎖可能會導致死鎖,需要特別注意死鎖檢測和處理。
 - 性能影響:由于每次操作都需要加鎖和解鎖,性能可能會受到影響,特別是在高并發(fā)情況下。
 
通過了解悲觀鎖的具體實現(xiàn),可以在需要嚴格數(shù)據(jù)一致性的場景中有效地避免并發(fā)沖突。
二、樂觀鎖
1.定義
樂觀鎖(Optimistic Lock)是一種假設沖突不會頻繁發(fā)生的鎖機制。每次數(shù)據(jù)訪問時,不會加鎖,而是在更新數(shù)據(jù)時檢查是否有其他線程修改過數(shù)據(jù)。如果檢測到?jīng)_突(數(shù)據(jù)被其他線程修改過),則重試操作或報錯。
樂觀鎖通常實現(xiàn)方式有以下兩種:
- 版本號機制:每次讀取數(shù)據(jù)時,讀取一個版本號,更新數(shù)據(jù)時,檢查版本號是否變化,如果沒有變化,則更新成功,否則重試。
 - 時間戳機制:類似版本號機制,通過時間戳來檢測數(shù)據(jù)是否被修改。
 
2.應用場景
適用于讀多寫少的場景,例如用戶評論系統(tǒng)、社交媒體點贊等,這些場景下并發(fā)沖突概率較低。
3.優(yōu)缺點
- 優(yōu)點:避免了頻繁的鎖操作,性能較好,適合讀多寫少的場景。
 - 缺點:在高并發(fā)寫操作的場景下,重試可能會頻繁發(fā)生,導致性能下降。
 
4.示例
樂觀鎖的實現(xiàn)通常涉及到版本號(或時間戳)機制,以便在更新數(shù)據(jù)時檢測是否發(fā)生了并發(fā)修改。我們還是用上面的示例,展示了如何在 Java中使用樂觀鎖進行并發(fā)控制。
假設有一個銀行賬戶表(Account),包含賬戶ID、余額和版本號三個字段,現(xiàn)在希望在更新賬戶余額時使用樂觀鎖,以確保數(shù)據(jù)的一致性。
整個運行流程總結為下面 3個步驟:
- 獲取賬戶信息,包括當前的版本號。
 - 計算新的余額,并增加版本號。
 - 嘗試更新賬戶信息,如果版本號匹配則更新成功,否則更新失敗并拋出異常。
 
(1) 數(shù)據(jù)庫表結構
CREATE TABLE Account (
    id INT PRIMARY KEY,
    balance DECIMAL(10, 2) NOT NULL,
    version INT NOT NULL
);(2) Java實現(xiàn)示例
Account類:
public class Account {
    private int id;
    private BigDecimal balance;
    private int version;
    // Getters and Setters
}AccountMapper接口:
public interface AccountMapper {
    Account getAccountById(int id);
    int updateAccount(Account account);
}AccountMapper的SQL實現(xiàn):
<mapper namespace="com.example.AccountMapper">
    <select id="getAccountById" resultType="com.example.Account">
        SELECT id, balance, version FROM Account WHERE id = #{id}
    </select>
    <update id="updateAccount">
        UPDATE Account
        SET balance = #{balance}, version = #{version}
        WHERE id = #{id} AND version = #{oldVersion}
    </update>
</mapper>AccountService類:
public class AccountService {
    private AccountMapper accountMapper;
    public AccountService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }
    public void updateAccountBalance(int accountId, BigDecimal amount) {
        // 獲取賬戶信息
        Account account = accountMapper.getAccountById(accountId);
        if (account == null) {
            throw new RuntimeException("Account not found");
        }
        // 記錄當前版本號
        int currentVersion = account.getVersion();
        // 更新余額
        account.setBalance(account.getBalance().add(amount));
        // 更新版本號
        account.setVersion(currentVersion + 1);
        // 嘗試更新賬戶信息
        int updatedRows = accountMapper.updateAccount(account);
        if (updatedRows == 0) {
            // 更新失敗,可能是由于并發(fā)修改導致的版本號不匹配
            throw new OptimisticLockException("Update failed due to concurrent modification");
        }
    }
}示例說明:
- Account類:包含賬戶ID、余額和版本號的Java類。
 - AccountMapper接口:定義了獲取賬戶信息和更新賬戶信息的方法。
 - AccountMapper的SQL實現(xiàn):使用MyBatis或其他ORM框架,定義了SQL查詢和更新語句。注意在更新語句中使用了舊版本號來檢測并發(fā)修改。
 - AccountService類:業(yè)務邏輯類,在更新賬戶余額時,先獲取當前賬戶信息及其版本號,然后嘗試更新余額和版本號。如果更新失敗,拋出一個OptimisticLockException。
 
三、區(qū)別總結
假設前提:
- 悲觀鎖假設沖突會頻繁發(fā)生,需要加鎖保護。
 - 樂觀鎖假設沖突不會頻繁發(fā)生,通過版本號或時間戳來檢測沖突。
 
性能:
- 悲觀鎖性能較低,因為每次操作都需要加鎖和解鎖。
 - 樂觀鎖性能較高,但在高并發(fā)寫操作下可能會頻繁重試,影響性能。
 
應用場景:
- 悲觀鎖適用于并發(fā)沖突高、數(shù)據(jù)一致性要求嚴格的場景。
 - 樂觀鎖適用于并發(fā)沖突低、讀多寫少的場景。
 
四、總結
本文我們詳細分析了悲觀鎖和樂觀鎖的原理、區(qū)別、實現(xiàn)方式和應用場景,實際工作中,可以根據(jù)具體需求選擇合適的并發(fā)控制機制,以保證系統(tǒng)的性能和數(shù)據(jù)一致性。















 
 
 








 
 
 
 