天天看點

悲觀鎖,樂觀鎖以及MVCC

在上文中,我們探讨了MySQL不同存儲引擎中的各類鎖,在這篇文章中我們将要讨論的是MySQL是如何實作并發控制的。并發問題有三種,分别為:

  1. 讀-讀,不存在任何問題
  2. 讀-寫,有隔離性問題,可能遇到髒讀(會讀到未送出的資料) ,幻影讀等。
  3. 寫-寫,可能丢失更新

首先我們先來看一下悲觀鎖和樂觀鎖:

  • 悲觀鎖:假定會發生并發沖突,屏蔽一切可能違反資料完整性的操作。

悲觀鎖(Pessimistic Lock)實際上是悲觀并發控制(Pessimistic Concurrency Control,“PCC”),顧名思義,就是對并發問題持有悲觀的态度,認為每次對資料的操作都會引發并發沖突,是以悲觀鎖每次操作資料的時候都會上鎖,以屏蔽一切可能違反資料完整性的操作。

悲觀鎖實際上就是運用了上一篇中提到的各類鎖來實作并發控制,較為簡單,但是開銷比較大,而且隻支援讀-讀并發,即對于同一個資料來說,如果采用悲觀鎖,那麼讀-寫和寫-寫并發是不被允許的。

  • 樂觀鎖:假設不會發生并發沖突,隻在送出操作時檢查是否違反資料完整性。

樂觀鎖(Optimistic Lock)實際上是樂觀并發控制(Optimistic Concurrency Control,“OCC”),對并發問題持有樂觀的态度,認為不會發生并發沖突,是以不會上鎖(是以樂觀鎖并不是一種鎖)。但是,在送出更新的時候會判斷一下在事務期間是否有其他程序更新了同一塊資料。樂觀鎖解決了寫-寫沖突的無鎖并發控制(注意,這邊的無鎖并不是真正的無鎖,而是在執行過程中不加鎖,在檢測是否沖突的時候還是需要對資料進行加鎖,但是這邊加鎖的時間明顯少了很多)。樂觀鎖一般來說有以下兩種實作方式:

1.使用資料版本(Version)記錄機制實作,這是樂觀鎖最常用的一種實作方式。何謂資料版本?即為資料增加一個版本辨別,一般是通過為資料庫表增加一個數字類型的 “version” 字段來實作。當讀取資料時,将version字段的值一同讀出,資料每更新一次,對此version值加一。當我們送出更新的時候,判斷資料庫表對應記錄的目前版本資訊與第一次取出來的version值進行比對,如果資料庫表目前版本号與第一次取出來的version值相等,則予以更新,否則認為是過期資料。用下面的一張圖來說明:

悲觀鎖,樂觀鎖以及MVCC

如上圖所示,如果更新操作順序執行,則資料的版本(version)依次遞增,不會産生沖突。但是如果發生有不同的業務操作對同一版本的資料進行修改,那麼,先送出的操作(圖中B)會把資料version更新為2,當A在B之後送出更新時發現資料的version已經被修改了,那麼A的更新操作會失敗。

2.使用時間戳(timestamp)。樂觀鎖定的第二種實作方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個字段,名稱無所謂,字段類型使用時間戳(timestamp), 和上面的version類似,也是在更新送出的時候檢查目前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一緻則OK,否則就是版本沖突。

  • 悲觀鎖和樂觀鎖的應用場景

從上文可以看到:

1. 當發生并發沖突的機率比較大時,悲觀鎖更合适,以提高事務的成功率。

2. 當發生并發沖突的機率小時(如讀多寫少),樂觀鎖更合适,可以提高系統的吞吐量。

接下來我們來看看MVCC是什麼。

MVCC的意思為多版本并發控制(Multiversion concurrency control),它解決的是讀-寫并發的問題。MVCC一般來說也可以看成是一種樂觀機制,和間隙鎖一樣,它可以用來解決幻讀的問題,隻是間隙鎖解決幻讀是用使寫程序阻塞的方式來進行的,而MVCC是以快照的方式來處理這一問題。不同資料庫版本對MVCC的實作機制不同,在這邊我們讨論InnoDB是如何進行MVCC的。

InnoDb 會為每一行記錄增加兩個字段,目前行建立時的版本号和删除時的版本号(可以為空),事務在寫一條記錄時會将其拷貝一份生成這條記錄的一個原始拷貝,寫操作同樣還是會對原記錄加鎖,但是讀操作會讀取未加鎖的新記錄,這就保證了讀寫并行。MVCC具體操作如下:

SELECT:InnoDB會根據以下兩個條件檢查每行記錄:

1)InnoDB隻查找版本早于目前事務版本的資料行(也就是,行的系統版本号小于或等于事務的系統版本号),這樣可以確定事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。

2)行的删除版本要麼未定義,要麼大于目前事務版本号。這可以確定事務讀取到的行,在事務開始之前未被删除。

INSERT:InnoDB為新插入的每一行儲存目前系統版本号作為行版本号。

DELETE:InnoDB為删除的每一行儲存目前系統版本号作為行删除辨別。

UPDATE:InnoDB為插入一行新記錄,儲存目前系統版本号作為行版本号,同時儲存當系統的版本号為原來的行作為删除辨別。

InnoDb 通過 MVCC 實作了讀寫并行,但是在不同的隔離級别下,讀的方式也是有所差別的。首先要特别指出的是,在 read uncommit 隔離級别下,每次都是讀取最新版本的資料行,是以不能用 MVCC 的多版本,而 serializable 隔離級别每次讀取操作都會為記錄加上讀鎖,也和 MVCC 不相容,是以隻有 RC 和 RR 這兩個隔離級别才有 MVCC。

盡管 RR 和 RC 隔離級别都實作了 MVCC 來滿足讀寫并行,但是讀的實作方式是不一樣的:RC 總是讀取記錄的最新版本,如果該記錄被鎖住,則讀取該記錄最新的一次快照,而 RR 是讀取該記錄事務開始時的那個版本。雖然這兩種讀取方式不一樣,但是它們讀取的都是快照資料,并不會被寫操作阻塞,是以這種讀操作稱為 快照讀(Snapshot Read)。快照讀在InnoBD的實作中就是普通不加鎖的select語句。與快照讀相對應的是目前讀,即處理的都是目前的資料,需要加鎖,如select  ... lock in share mode,for update以及select,update和delete,在解決目前讀的幻讀問題時,MySQL使用了間隙鎖的機制。

 參考文檔:

https://liuzhengyang.github.io/2017/04/18/innodb-mvcc/

http://www.cnblogs.com/chenpingzhao/p/5065316.html

https://riverdba.github.io/2017/04/01/MVCC-theory-study/

繼續閱讀