MySQL 樂觀鎖&悲觀鎖

悲觀鎖(Pessimistic Lock)

悲觀鎖的特點是先獲取鎖,再進行業務操作,即“悲觀”的認爲獲取鎖是非常有可能失敗的,因此要先確保獲取鎖成功再進行業務操作。通常所說的“一鎖二查三更新”即指的是使用悲觀鎖。通常來講在數據庫上的悲觀鎖需要數據庫本身提供支持,即通過常用的select … for update操作來實現悲觀鎖。當數據庫執行select for update時會獲取被select中的數據行的行鎖,因此其他併發執行的select for update如果試圖選中同一行則會發生排斥(需要等待行鎖被釋放),因此達到鎖的效果。select for update獲取的行鎖會在當前事務結束時自動釋放,因此必須在事務中使用。

這裏需要注意的一點是不同的數據庫對select for update的實現和支持都是有所區別的,例如oracle支持select for update no wait,表示如果拿不到鎖立刻報錯,而不是等待,mysql就沒有no wait這個選項。另外mysql還有個問題是select for update語句執行中所有掃描過的行都會被鎖上,這一點很容易造成問題。因此如果在mysql中用悲觀鎖務必要確定走了索引,而不是全表掃描。

傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。再比如Java裏面的同步原語synchronized關鍵字的實現也是悲觀鎖。

 

樂觀鎖(Optimistic Lock)

樂觀鎖的特點先進行業務操作,不到萬不得已不去拿鎖。即“樂觀”的認爲拿鎖多半是會成功的,因此在進行完業務操作需要實際更新數據的最後一步再去拿一下鎖就好。

樂觀鎖在數據庫上的實現完全是邏輯的,不需要數據庫提供特殊的支持。一般的做法是在需要鎖的數據上增加一個版本號,或者時間戳,然後按照如下方式實現:

1. SELECT data AS old_data, version AS old_version FROM …;
2. 根據獲取的數據進行業務操作,得到new_data和new_version
3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
    // 樂觀鎖獲取成功,操作完成
} else {
    // 樂觀鎖獲取失敗,回滾並重試
}

樂觀鎖是否在事務中其實都是無所謂的,其底層機制是這樣:在數據庫內部update同一行的時候是不允許併發的,即數據庫每次執行一條update語句時會獲取被update行的寫鎖,直到這一行被成功更新後才釋放。因此在業務操作進行前獲取需要鎖的數據的當前版本號,然後實際更新數據時再次對比版本號確認與之前獲取的相同,並更新版本號,即可確認這之間沒有發生併發的修改。如果更新失敗即可認爲老版本的數據已經被併發修改掉而不存在了,此時認爲獲取鎖失敗,需要回滾整個業務操作並可根據需要重試整個過程。

樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫提供的類似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。

 

CAS算法

即compare and swap(比較與交換),是一種有名的無鎖算法。無鎖編程,即不使用鎖的情況下實現多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實現變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三個操作數

需要讀寫的內存值 V
進行比較的值 A
擬寫入的新值 B
當且僅當 V 的值等於 A時,CAS通過原子方式用新值B來更新V的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試。

 

樂觀鎖的缺點

1. ABA 問題

如果一個變量V初次讀取的時候是A值,並且在準備賦值的時候檢查到它仍然是A值,那我們就能說明它的值沒有被其他線程修改過了嗎?很明顯是不能的,因爲在這段時間它的值可能被改爲其他值,然後又改回A,那CAS操作就會誤認爲它從來沒有被修改過。這個問題被稱爲CAS操作的 "ABA"問題。

JDK 1.5 以後的 AtomicStampedReference 類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

2. 循環時間長開銷大
自旋CAS(也就是不成功就一直循環執行直到成功)如果長時間不成功,會給CPU帶來非常大的執行開銷。 如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

3. 只能保證一個共享變量的原子操作

CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。但是從 JDK 1.5開始,提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象裏來進行 CAS 操作.所以我們可以使用鎖或者利用AtomicReference類把多個共享變量合併成一個共享變量來操作。

 

CAS與synchronized的使用情景

簡單的來說CAS適用於寫比較少的情況下(多讀場景,衝突一般較少),synchronized適用於寫比較多的情況下(多寫場景,衝突一般較多)

對於資源競爭較少(線程衝突較輕)的情況,使用synchronized同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗cpu資源;而CAS基於硬件實現,不需要進入內核,不需要切換線程,操作自旋機率較少,因此可以獲得更高的性能。
對於資源競爭嚴重(線程衝突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低於synchronized。
補充: Java併發編程這個領域中synchronized關鍵字一直都是元老級的角色,很久之前很多人都會稱它爲 “重量級鎖” 。但是,在JavaSE 1.6之後進行了主要包括爲了減少獲得鎖和釋放鎖帶來的性能消耗而引入的 偏向鎖 和 輕量級鎖 以及其它各種優化之後變得在某些情況下並不是那麼重了。synchronized的底層實現主要依靠 Lock-Free 的隊列,基本思路是 自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。在線程衝突較少的情況下,可以獲得和CAS類似的性能;而線程衝突嚴重的情況下,性能遠高於CAS。

 

比較

(沒有切身體會,看心情在補充)樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量

 

使用實例:

1. MySQL的事務支持 

MySQL的事務支持不是綁定在MySQL服務器本身,而是與存儲引擎相關

  1. MyISAM:不支持事務,用於只讀程序提高性能   
  2. InnoDB:支持ACID事務、行級鎖、併發   
  3. Berkeley DB:支持事務  


2. 隔離級別

隔離級別決定了一個session中的事務可能對另一個session的影響、併發session對數據庫的操作、一個session中所見數據的一致性 
ANSI標準定義了4個隔離級別,MySQL的InnoDB都支持:

  1. READ UNCOMMITTED:最低級別的隔離,通常又稱爲dirty read,它允許一個事務讀取還沒commit的數據,這樣可能會提高性能,但是dirty read可能不是我們想要的   
  2. READ COMMITTED:在一個事務中只允許已經commit的記錄可見,如果session中select還在查詢中,另一session此時insert一條記錄,則新添加的數據不可見   
  3. REPEATABLE READ:在一個事務開始後,其他session對數據庫的修改在本事務中不可見,直到本事務commit或rollback。在一個事務中重複select的結果一樣,除非本事務中update數據庫。   
  4. SERIALIZABLE:最高級別的隔離,只允許事務串行執行。爲了達到此目的,數據庫會鎖住每行已經讀取的記錄,其他session不能修改數據直到前一事務結束,事務commit或取消時才釋放鎖。  

 

可以使用如下語句設置MySQL的session隔離級別:

SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}   

 MySQL默認的隔離級別是REPEATABLE READ,在設置隔離級別爲READ UNCOMMITTED或SERIALIZABLE時要小心,READ UNCOMMITTED會導致數據完整性的嚴重問題,而SERIALIZABLE會導致性能問題並增加死鎖的機率

3,隔離級別

樂觀鎖和悲觀鎖策略: 
悲觀鎖:在讀取數據時鎖住那幾行,其他對這幾行的更新需要等到悲觀鎖結束時才能繼續 
樂觀鎖:讀取數據時不鎖,更新時檢查是否數據已經被更新過,如果是則取消當前更新 
一般在悲觀鎖的等待時間過長而不能接受時我們纔會選擇樂觀鎖 

 

悲觀鎖例子

CREATE PROCEDURE tfer_funds     
       (from_account INT, to_account INT,tfer_amount NUMERIC(10,2),     
        OUT status INT, OUT message VARCHAR(30))     
BEGIN     
    DECLARE from_account_balance NUMERIC(10,2);     
    
    START TRANSACTION;     
    
    
    SELECT balance     
      INTO from_account_balance     
      FROM account_balance     
     WHERE account_id=from_account     
       FOR UPDATE;     
    
    IF from_account_balance>=tfer_amount THEN     
    
         UPDATE account_balance     
            SET balance=balance-tfer_amount     
          WHERE account_id=from_account;     
    
         UPDATE account_balance     
            SET balance=balance+tfer_amount     
          WHERE account_id=to_account;     
         COMMIT;     
    
         SET status=0;     
         SET message='OK';     
    ELSE     
         ROLLBACK;     
         SET status=-1;     
         SET message='Insufficient funds';     
    END IF;     
END;    

 

樂觀鎖例子

CREATE PROCEDURE tfer_funds     
    (from_account INT, to_account INT, tfer_amount NUMERIC(10,2),     
        OUT status INT, OUT message VARCHAR(30) )     
    
BEGIN     
    
    DECLARE from_account_balance    NUMERIC(8,2);     
    DECLARE from_account_balance2   NUMERIC(8,2);     
    DECLARE from_account_timestamp1 TIMESTAMP;     
    DECLARE from_account_timestamp2 TIMESTAMP;     
    
    SELECT account_timestamp,balance     
        INTO from_account_timestamp1,from_account_balance     
            FROM account_balance     
            WHERE account_id=from_account;     
    
    IF (from_account_balance>=tfer_amount) THEN     
    
        -- Here we perform some long running validation that     
        -- might take a few minutes */     
        CALL long_running_validation(from_account);     
    
        START TRANSACTION;     
    
        -- Make sure the account row has not been updated since     
        -- our initial check     
        SELECT account_timestamp, balance     
            INTO from_account_timestamp2,from_account_balance2     
            FROM account_balance     
            WHERE account_id=from_account     
            FOR UPDATE;     
    
        IF (from_account_timestamp1 <> from_account_timestamp2 OR     
            from_account_balance    <> from_account_balance2)  THEN     
            ROLLBACK;     
            SET status=-1;     
            SET message=CONCAT("Transaction cancelled due to concurrent update",     
                " of account"  ,from_account);     
        ELSE     
            UPDATE account_balance     
                SET balance=balance-tfer_amount     
                WHERE account_id=from_account;     
    
            UPDATE account_balance     
                SET balance=balance+tfer_amount     
                WHERE account_id=to_account;     
    
            COMMIT;     
    
            SET status=0;     
            SET message="OK";     
        END IF;     
    
    ELSE     
        ROLLBACK;     
        SET status=-1;     
        SET message="Insufficient funds";     
    END IF;     
END$$    

 

參考文章

面試必備之樂觀鎖與悲觀鎖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章