InnoDB的MVCC實現原理(InnoDB如何實現MVCC以及MVCC的工作機制)

MVCC(多版本併發控制)

使用鎖和鎖協議來實現相應的隔離級別來進行併發控制,味道雖好但因爲鎖會造成事務阻塞,導致併發性能會受到一定的影響。而多版本併發控制使得對同一行記錄做讀寫的事務之間不用相互阻塞等待(寫寫還是要阻塞等待,因爲事務對數據進行更新時會加上排他鎖),提高了事務的併發能力,可以認爲MVCC是一種解決讀寫阻塞等待的行級鎖。

MVCC是多版本併發控制機制,顧名思義支持MVCC的數據庫表中每一行數據都可能存在多個版本,對數據庫的任何修改的提交都不會直接覆蓋之前的數據,而是產生一個新的版本與老版本共存,通過讀寫數據時讀不同的版本來避免加鎖阻塞。不同的存儲引擎實現的MVCC多少有些差異,這裏主要討論下Mysql的默認存儲引擎InnoDB對MVCC的實現原理。MVCC的實現主要依賴於數據庫在每個表中添加的三個隱藏字段以及事務在查詢時創建的快照(read view)和數據庫的數據版本鏈(Undo log)。這裏先介紹這三個部分的作用,然後再介紹它們是如何聯合作戰進行非阻塞的實現RC和RR隔離級別。

MVCC的重要特性:
(1)MVCC只支持RC(讀取已提交)和RR(可重複讀)隔離級別。
(2)MVCC能解決髒讀、不可重複讀問題,不能解決丟失更新問題和幻讀問題。
(3)MVCC是用來解決讀寫操作之間的阻塞問題。使得在併發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫併發讀寫的性能。

三個隱藏字段

InnoDB會爲每個使用InnoDB存儲引擎的表添加三個隱藏字段,用於實現數據多版本以及聚集索引,他們的作用如下:

DB_TRX_ID(6字節): 它是最近一次更新或者插入或者刪除該行數據的事務ID(若是刪除,則該行有一個刪除位更新爲已刪除。但並不是真正的進行物理刪除,當InnoDB丟棄爲刪除而編寫的更新撤消日誌記錄時,它纔會物理刪除相應的行及其索引記錄。此刪除操作稱爲清除,速度非常快)
DB_ROLL_PTR(7字節): 回滾指針,指向當前記錄行的undo log信息(指向該數據的前一個版本數據)
DB_ROW_ID(6字節): 隨着新行插入而單調遞增的行ID。InnoDB使用聚集索引,數據存儲是以聚集索引字段的大小順序進行存儲的,當表沒有主鍵或唯一非空索引時,innodb就會使用這個行ID自動產生聚簇索引。如果表有主鍵或唯一非空索引,聚簇索引就不會包含這個行ID了。這個DB_ROW_ID跟MVCC關係不大。

Read View

read view是讀視圖,其實就相當於一種快照,主要用途是用來做可見性判斷,判斷當前事務是否有資格訪問該行數據(詳情下解)。read view有多個變量,這裏將關鍵變量進行描述,爲下文做鋪墊。

trx_ids: 它裏面的trx_ids變量存儲了活躍事務列表,也就是Read View開始創建時其他未提交的活躍事務的ID列表。例如事務A在創建read view(快照)時,數據庫中事務B和事務C還沒提交或者回滾結束事務,此時trx_ids就會將事務B和事務C的事務ID記錄下來。
low_limit_id: 目前出現過的最大的事務ID+1,即下一個將被分配的事務ID。
up_limit_id: 活躍事務列表trx_ids中最小的事務ID,如果trx_ids爲空,則up_limit_id 爲 low_limit_id,雖然該字段名爲up_limit,但在trx_ids中的活躍事務號是降序的,所以最後一個爲最小活躍事務ID。
creator_trx_id: 當前創建read view的事務的ID。

Undo log

Undo log中存儲的是老版本數據,當一個事務需要讀取記錄行時,如果當前記錄行不可見,可以通過回滾指針順着undo log鏈找到滿足其可見性條件的記錄行版本。

在InnoDB裏,undo log分爲如下兩類:
①insert undo log : 事務對insert新記錄時產生的undo log, 只在事務回滾時需要, 並且在事務提交後就可以立即丟棄。
②update undo log : 事務對記錄進行delete和update操作時產生的undo log,不僅在事務回滾時需要,快照讀也需要,只有當數據庫所使用的快照中不涉及該日誌記錄,對應的回滾日誌纔會被purge線程刪除。

Purge線程:上文提到了InnoDB刪除一個行記錄時,並不是立刻物理刪除,而是將該行數據的DB_TRX_ID字段更新爲做刪除操作的事務ID,並將刪除位deleted_bit設置爲true(已刪除),將其放入update undo log中。爲了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit爲true的記錄。purge線程自己也維護了一個read view,如果某個記錄的deleted_bit爲true,並且DB_TRX_ID相對於purge線程的read view可見,那麼這條記錄一定是可以被安全清除的。

MVCC更新操作的實現原理

MVCC機制下實現更新還是會用到排他鎖,但由於我們讀的時候可以通過快照讀,讀多個版本避免了使用共享鎖,因此可以使得讀事務不會因爲寫事務阻塞。MVCC的優越性在於事務需要讀行記錄的時候不會因爲有事務在更新該行記錄而阻塞,事務在寫行記錄時也不會因爲有事務在讀數據而阻塞。

更新原理: 假設我現在需要修改行記錄A,他們的修改過程如下,
(1)MVCC更新行記錄A時會先用排他鎖鎖住該行記錄A;
(2)然後將該行記錄複製到update undo log中,生成舊版本行記錄B;
(3)使行記錄A的回滾指針指向這條舊版本B,再在行記錄A中修改 用戶需要修改的字段,並將DB_TRX_ID字段更新爲更新這條記錄的事務ID;
(4)最後提交事務。(用戶需要修改的字段指的是業務字段,比如我們要修改name等)

通過回滾指針,形成了一條當前行記錄指向歷代舊版本行記錄的鏈表,通過這條鏈表,我們就可以查詢該行記錄的多箇舊版本。
在這裏插入圖片描述

MVCC查詢操作的實現原理

InnoDB中,事務在第一次進行普通的select查詢時,會創建一個read view(快照),用於可見性判斷,事務只能查詢到行記錄對於事務來說可見的數據版本。可見性判斷是通過行記錄的DB_TRX_ID(最近一次插入/更新/刪除該行記錄的事務ID)以及read view中的變量比較來判斷。

查詢過程如下:
(1) 如果 DB_TRX_ID< up_limit_id,則表明這個行記錄最近一次更新在當前事務創建快照之前就已經提交了,該記錄行的值對當前事務是可見的,當前事務可以訪問該行記錄,跳到步驟(4)。
(2) 如果DB_TRX_ID>=low_limit_id,則表明這個行記錄最近一次更新是快照創建之後才創建的事務完成的,該記錄行的值對當前事務是不可見的,當前事務不可以訪問該行記錄。因此當前事務只能訪問比該行記錄更舊的數據版本。通過該記錄行的 DB_ROLL_PTR 指針,找到更舊一版的行記錄,取出更舊一版的行記錄的事務號DB_TRX_ID,然後跳到步驟(1)重新判斷當前事務是否有資格訪問該行記錄。
(3) 如果up_limit_id<=DB_TRX_ID< low_limit_id,則表明對這個行記錄最近一次更新的事務可能是活躍列表中的事務也可能是已經成功提交的事務(事務ID號大的事務可能會比ID號小的事務先進行提交),比如說初始時有5個事務在併發執行,事務ID分別是1001~1005,1004事務完成提交,1001事務進行普通select的時候創建的快照中活躍事務列表就是1002、1003、1005。因此up_limit_id就是1002, low_limit_id就是1006。對於這種情況,我們需要在活躍事務列表中進行遍歷(因爲活躍事務列表中的事務ID是有序的,因此用二分查找),確定DB_TRX_ID是否在活躍事務列表中。
(3.1)若不在,說明對這個行記錄最近一次更新的事務是在創建快照之前提交的事務,此行記錄對當前事務是可見的,也就是說當前事務有資格訪問此行記錄,跳到步驟(4)。
(3.2)若在,說明對這個行記錄最近一次更新的事務是當前活躍事務,在快照創建過程中或者之後完成的數據更新,此行記錄對當前事務是不可見的(若可見則會造成髒讀、不可重複讀等問題)。因此當前事務只能訪問該行記錄的更舊的版本數據。通過該記錄行的 DB_ROLL_PTR 指針,找到更舊一版的行記錄,取出更舊一版的行記錄的事務號DB_TRX_ID,然後跳到步驟(1)重新判斷當前事務是否有資格訪問該行記錄。
(4)可以訪問,將該行記錄的值返回。

當前讀和快照讀

快照讀:使用普通的select 語句進行查詢時會生成快照,進行快照讀,快照讀不會上鎖,根據可見性判斷,來決定是讀取該行記錄的最新版本還是舊版本。(只有使用普通的select語句進行查詢纔會用到快照讀,才享受到了MVCC機制的讀寫非阻塞的優越性)

當前讀:使用select … lock in share mode,select … for update,insert,update,delete 語句等語句進行查詢或者更新時,會使用相應的鎖進行鎖定,查詢到的肯定數據庫中該行記錄的最新版本。

MVCC如何實現RC和RR

MVCC對兩個隔離級別實現的差異在其產生的read view(快照)的次數不同。

RC:讀取已提交隔離級別,避免了髒讀,存在不可重複讀、幻讀問題。MVCC對該級別的實現就是每次進行普通的select查詢,都會產生一個新的快照(不同時間,當前活躍的事務不同,行記錄最近一次更新的事務ID也可能不同)。就相當於二級鎖協議,進行讀操作需要加讀鎖,讀完就釋放鎖,雖然併發性更好且避免了髒讀,但會存在不可重複讀。

RR:可重複讀隔離級別,避免了髒讀和不可重複讀,存在幻讀問題。MVCC對該級別的實現就是在當前事務中只有第一次進行普通的select查詢,纔會產生快照,此後這個事務一直使用這一個快照進行快照查,相當於三級鎖協議,進行讀操作需要加讀鎖,事務結束才釋放。避免了不可重複讀。但存在幻讀,禁止幻讀可以通過Next-Key Locks算法的間隙鎖和記錄鎖實現。

借(偷)來一個例子來對上面講述的MVCC知識進行具體說明
在這裏插入圖片描述
在這裏插入圖片描述

總結

1、MVCC的實現主要依賴於數據庫在每個表中添加的三個隱藏字段以及事務在查詢時創建的快照(read view)和數據庫支持多版本數據,數據庫中存在數據版本鏈(Undo log)。
2、InnoDB支持多版本數據,在更新或者刪除數據時,並不會立馬刪除原有行記錄,而是將舊版本存入回滾段中的Undo log內,並通過回滾指針形成一個數據鏈,可以通過這個指針訪問鏈上的歷代數據版本,正是這種機制使得數據庫數據產生了多個版本,爲通過MVCC進行快照讀提供了可能。
3、並不是所有的查詢都是進行快照讀,使用普通的select 語句進行查詢時會生成快照,進行快照讀;使用select … lock in share mode,select … for update,insert,update,delete 語句等語句進行查詢或者更新時還是會使用鎖機制,進行鎖阻塞。
4、使用MVCC的作用(意義)是非阻塞的解決了事務讀寫衝突,提高了併發性能。
5、MVCC只支持RC(讀取已提交)和RR(可重複讀)隔離級別。
6、MVCC能解決髒讀、不可重複讀問題,不能解決丟失更新問題和幻讀問題。
7、InnoDB使用鎖機制和MVCC來共同作用,進行併發控制的,雖然MVCC不能解決幻讀和丟失更新問題,但通過與鎖機制(行級鎖的Next-Key Locks算法的使用、排他鎖等)一起作用可以達到可串行化隔離級別的效果,禁止了幻讀、更新丟失等問題。

參照:
[MySQL官方文檔]
MySQL中MVCC的正確打開方式(源碼佐證)

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