一文讀懂資料庫中的樂觀鎖和悲觀鎖和MVVC

X先生發表於2020-07-22

前言

在資料庫的實際使用過程中,我們常常會遇到不希望資料被同時寫或者讀的情景,例如秒殺場景下,兩個請求同時讀到系統還有庫存1個,然後又先後把庫存更新為0,這時候就會出現超賣的情況,這時候貨物的實際庫存和我們的記錄就會對應不上了。

為了解決這種資源競爭導致的資料不一致等問題,我們需要有一種機制來進行保證資料的正確訪問和修改,而在資料庫中,這種機制就是資料庫的併發控制。其中樂觀併發控制,悲觀併發控制和多版本併發控制是資料庫併發控制主要採用的技術手段。

悲觀併發控制

本質

維基百科:在關聯式資料庫管理系統裡,悲觀併發控制(又名“悲觀鎖”,Pessimistic Concurrency Control,縮寫“PCC”)是一種併發控制的方法。它可以阻止一個事務以影響其他使用者的方式來修改資料。如果一個事務執行的操作讀某行資料應用了鎖,那只有當這個事務把鎖釋放,其他事務才能夠執行與該鎖衝突的操作。

事實上我們常說的悲觀鎖並不是一種實際的鎖,而是一種併發控制的思想,悲觀併發控制對於資料被修改持悲觀的態度,認為資料被外界訪問時,必然會產生衝突,所以在資料處理的過程中都採用加鎖的方式來保證對資源的獨佔。

資料庫的鎖機制其實都是基於悲觀併發控制的觀點進行實現的,而且按照實際使用情況,資料庫的鎖又可以分為許多種類,具體可以見我後面的文章。

實現方式

資料庫悲觀鎖的加鎖流程大致如下:

  • 開始事務後,按照操作型別給需要加鎖的資料申請加某一類鎖:例如共享行鎖等
  • 加鎖成功則繼續後面的操作,如果資料已經被加了其他的鎖,而且和現在要加的鎖衝突,則會加鎖失敗(例如已經加了排他鎖),此時需等待其他的鎖釋放(可能出現死鎖)
  • 完成事務後釋放所加的鎖

優缺點

優點:
悲觀併發控制採取的是保守策略:“先取鎖,成功了才訪問資料”,這保證了資料獲取和修改都是有序進行的,因此適合在寫多讀少的環境中使用。當然使用悲觀鎖無法維持非常高的效能,但是在樂觀鎖也無法提供更好的效能前提下,悲觀鎖卻可以做到保證資料的安全性。

缺點:
由於需要加鎖,而且可能面臨鎖衝突甚至死鎖的問題,悲觀併發控制增加了系統的額外開銷,降低了系統的效率,同時也會降低了系統的並行性。

樂觀併發控制

本質

維基百科:在關聯式資料庫管理系統裡,樂觀併發控制(又名“樂觀鎖”,Optimistic Concurrency Control,縮寫“OCC”)是一種併發控制的方法。它假設多使用者併發的事務在處理時不會彼此互相影響,各事務能夠在不產生鎖的情況下處理各自影響的那部分資料。

樂觀併發控制對資料修改持樂觀態度,認為即使在併發環境中,外界對資料的操作一般是不會造成衝突,所以並不會去加鎖,而是在提交資料更新之前,每個事務會先檢查在該事務讀取資料後,有沒有其他事務又修改了該資料。如果其他事務有更新的話,則讓返回衝突資訊,讓使用者決定如何去做下一步,比如說重試或者回滾。

可以看出,樂觀鎖其實也不是實際的鎖,甚至沒有用到鎖來實現併發控制,而是採取其他方式來判斷能否修改資料。樂觀鎖一般是使用者自己實現的一種鎖機制,雖然沒有用到實際的鎖,但是能產生加鎖的效果。

實現方式

CAS(比較與交換,Compare and swap) 是一種有名的無鎖演算法。無鎖程式設計,即不使用鎖的情況下實現多執行緒之間的變數同步,也就是在沒有執行緒被阻塞的情況下實現變數的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。實現非阻塞同步的方案稱為“無鎖程式設計演算法”( Non-blocking algorithm)。

樂觀鎖基本都是基於 CAS(Compare and swap)演算法來實現的。我們先來看下CAS過程,一個CAS操作的過程可以用以下c程式碼表示:

int cas(long *addr, long old, long new)
{
    /* Executes atomically. */
    if(*addr != old)
        return 0;
    *addr = new;
    return 1;
}

CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。整個CAS操作是一個原子操作,是不可分割的。

樂觀鎖的實現就類似於上面的過程,主要有以下幾種方式:

  • 版本號標記:在表中新增一個欄位:version,用於儲存版本號。獲取資料的時候同時獲取版本號,然後更新資料的時候用以下命令:update xxx set version=version+1,… where … version="old version" and ....。這時候通過判斷返回結果的影響行數是否為0來判斷是否更新成功,更新失敗則說明有其他請求已經更新了資料了。
  • 時間戳標記:和版本號一樣,只是通過時間戳來判斷。一般來說很多資料表都會有更新時間這一個欄位,通過這個欄位來判斷就不用再新增一個欄位了。
  • 待更新欄位:如果沒有時間戳欄位,而且不想新增欄位,那可以考慮用待更新欄位來判斷,因為更新資料一般都會發生變化,那更新前可以拿要更新的欄位的舊值和資料庫的現值進行比對,沒有變化則更新。
  • 所有欄位標記:資料表所有欄位都用來判斷。這種相當於就、不僅僅對某幾個欄位做加鎖了,而是對整個資料行加鎖,只要本行資料發生變化,就不進行更新。

優缺點

優點:
樂觀併發控制沒有實際加鎖,所以沒有額外開銷,也不錯出現死鎖問題,適用於讀多寫少的併發場景,因為沒有額外開銷,所以能極大提高資料庫的效能。

缺點:
樂觀併發控制不適合於寫多讀少的併發場景下,因為會出現很多的寫衝突,導致資料寫入要多次等待重試,在這種情況下,其開銷實際上是比悲觀鎖更高的。而且樂觀鎖的業務邏輯比悲觀鎖要更為複雜,業務邏輯上要考慮到失敗,等待重試的情況,而且也無法避免其他第三方系統對資料庫的直接修改的情況。

多版本併發控制

本質

維基百科: 多版本併發控制(Multiversion concurrency control, MCC 或 MVCC),是資料庫管理系統常用的一種併發控制,也用於程式設計語言實現事務記憶體。

樂觀併發控制和悲觀併發控制都是通過延遲或者終止相應的事務來解決事務之間的競爭條件來保證事務的可序列化;雖然前面的兩種併發控制機制確實能夠從根本上解決併發事務的可序列化的問題,但是其實都是在解決寫衝突的問題,兩者區別在於對寫衝突的樂觀程度不同(悲觀鎖也能解決讀寫衝突問題,但是效能就一般了)。而在實際使用過程中,資料庫讀請求是寫請求的很多倍,我們如果能解決讀寫併發的問題的話,就能更大地提高資料庫的讀效能,而這就是多版本併發控制所能做到的事情。

與悲觀併發控制和樂觀併發控制不同的是,MVCC是為了解決讀寫鎖造成的多個、長時間的讀操作餓死寫操作問題,也就是解決讀寫衝突的問題。MVCC 可以與前兩者中的任意一種機制結合使用,以提高資料庫的讀效能。

資料庫的悲觀鎖基於提升併發效能的考慮,一般都同時實現了多版本併發控制。不僅是MySQL,包括Oracle、PostgreSQL等其他資料庫系統也都實現了MVCC,但各自的實現機制不盡相同,因為MVCC沒有一個統一的實現標準。

總的來說,MVCC的出現就是資料庫不滿用悲觀鎖去解決讀-寫衝突問題,因效能不高而提出的解決方案。

實現方式

MVCC的實現,是通過儲存資料在某個時間點的快照來實現的。每個事務讀到的資料項都是一個歷史快照,被稱為快照讀,不同於當前讀的是快照讀讀到的資料可能不是最新的,但是快照隔離能使得在整個事務看到的資料都是它啟動時的資料狀態。而寫操作不覆蓋已有資料項,而是建立一個新的版本,直至所在事務提交時才變為可見。

當前讀和快照讀

什麼是MySQL InnoDB下的當前讀和快照讀?

當前讀
像select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操作都是一種當前讀,為什麼叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。

快照讀
像不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是未提交讀和序列化級別,因為未提交讀總是讀取最新的資料行,而不是符合當前事務版本的資料行。而序列化則會對所有讀取的行都加鎖

優缺點

MVCC 使大多數讀操作都可以不用加鎖,這樣設計使得讀資料操作很簡單,效能很好,並且也能保證只會讀取到符合標準的行。不足之處是每行記錄都需要額外的儲存空間,需要做更多的行檢查工作,以及一些額外的維護工作。

適用場景

  • 悲觀鎖

    • 用來解決讀-寫衝突和寫-寫衝突的的加鎖併發控制
    • 適用於寫多讀少,寫衝突嚴重的情況,因為悲觀鎖是在讀取資料的時候就加鎖的,讀多的場景會需要頻繁的加鎖和很多的的等待時間,而在寫衝突嚴重的情況下使用悲觀鎖可以保證資料的一致性
    • 資料一致性要求高
    • 可以解決髒讀,幻讀,不可重複讀,第一類更新丟失,第二類更新丟失的問題
  • 樂觀鎖

    • 解決寫-寫衝突的無鎖併發控制
    • 適用於讀多寫少,因為如果出現大量的寫操作,寫衝突的可能性就會增大,業務層需要不斷重試,這會大大降低系統效能
    • 資料一致性要求不高,但要求非常高的響應速度
    • 無法解決髒讀,幻讀,不可重複讀,但是可以解決更新丟失問題
  • MVCC

    • 解決讀-寫衝突的無鎖併發控制
    • 與上面兩者結合,提升它們的讀效能
    • 可以解決髒讀,幻讀,不可重複讀等事務問題,更新丟失問題除外

參考資料

維基百科
https://www.cnblogs.com/rinack/p/10032207.html
https://draveness.me/database-concurrency-control/

版權宣告

轉載請註明作者和文章出處
作者: X先生
https://segmentfault.com/a/1190000023332101

相關文章