一文搞懂MySQL事務的隔離性如何實現|MVCC

白澤來了發表於2022-04-11

關注公眾號【程式設計師白澤】,帶你走進一個不一樣的程式設計師/學生黨

前言

MySQL有ACID四大特性,本文著重講解MySQL不同事務之間的隔離性的概念,以及MySQL如何實現隔離性。下面先羅列一下MySQL的四種事務隔離級別,以及不同隔離級別可能會存在的問題。事務隔離級別越高,多個事務在併發訪問資料庫時互相產生資料干擾的可能性越低,但是併發訪問的效能就越差。(相當於犧牲了一定的效能去保證資料的安全性)

下面這張表,展示了MySQL的四大隔離級別和伴隨著的一些問題,下面詳細介紹。

image-20220411111938966

事務隔離級別

讀未提交:多個事務同時修改一條記錄,A事務對其的改動在A事務還沒提交時,在B事務中就可以看到A事務對其的改動。

讀已提交:多個事務同時修改一條記錄,A事務對其的改動在A事務提交之後,在B事務中可以看到A事務對其的改動。

可重複讀:多個事務同時修改一條記錄,這條記錄在A事務執行期間是不變的(別的事務對這條記錄的修改不被A事務感知)。

序列化:多個事務同時訪問一條記錄(CRUD),讀加讀鎖,寫加寫鎖,完全退化成了序列的訪問,自然不會收到任何其他事務的干擾,效能最低。

不同級別伴隨的問題

髒讀:A事務在提交前對一個欄位的改動會被B事務感知,那麼事務之間就很容易產生干擾,假如A對一個欄位改動之後被B感知,但是A又回滾了事務,則對該欄位的改動依舊保留在B的查詢結果中,那麼這樣的資料就是髒資料(處於處理中間過程的資料)。

不可重複讀:A事務對於一條記錄的讀取結果,在B事務對其修改並提交之後,A再次讀取同一條記錄會得到不同的結果。

幻讀:側重於A事務的同一個範圍查詢命令,前後兩次得到不同的記錄數量,原因是B事務可能對其進行了插入。

小結一下

通過閱讀上面給出的內容,可以得到結論:

  1. 讀未提交隔離級別並沒有對行資料的可見性做任何限制,所有事務之間的改動都是互相可見的,所以存在很多問題,不推薦使用;
  1. 序列化隔離級別因為通過鎖機制對記錄的訪問進行限制,所以安全性最高,但併發訪問退化成序列訪問,效能較低;

因此本文將側重於探究MySQL如何實現讀已提交可重複讀兩種隔離級別(也就是你聽聞的MVCC多版本併發控制的實現),通過後面的學習你將理解讀已提交隔離級別如何解決髒讀可重複讀隔離級別如何更進一步解決不可重複讀

接下來我將向你介紹undo 版本鏈機制以及read view快照讀機制,這兩個機制相互配合是實現MVCC的核心,而讀已提交可重複讀隔離級別的實現都是建立在這兩個核心機制之上。

undo 版本鏈

undo 版本鏈就是指undo log的儲存在邏輯上的表現形式,它被用於事務當中的回滾操作以及實現MVCC,這裡介紹一下undo log之所以能實現回滾記錄的原理。

對於每一行記錄,會有兩個隱藏欄位:row_trx_idroll_pointerrow_trx_id表示更新(改動)本條記錄的全域性事務id (每個事務建立都會分配id,全域性遞增,因此事務id區別對某條記錄的修改是由哪個事務作出的)roll_pointer是回滾指標,指向當前記錄的前一個undo log版本,如果是第一個版本則roll_pointer指向nil,這樣如果有多個事務對同一條記錄進行了多次改動,則會在undo log中以鏈的形式儲存改動過程。

假如有兩個事務AB,資料表中有一行id為1的記錄,其欄位a初始值為0,事務A對id=1的行的a修改為1,事務B對id=1的行的a欄位修改為2,則undo log版本鏈記錄如下:

image-20220410185551723

在上圖中,最下方的undo log中記錄了當前行的最新版本,而該條記錄之前的版本則以版本鏈的形式可追溯,這也是事務回滾所做的事。那undo log版本鏈和事務的隔離性有什麼關係呢?那就要引入另一個核心機制:read view。

read view

read view表示快照讀,這個快照讀會記錄四個關鍵的屬性:

  1. create_trx_id: 當前事務的id
  2. m_idx: 當前正在活躍的所有事務id(id陣列),沒有提交的事務的id
  3. min_trx_id: 當前系統中活躍的事務的id最小值
  4. max_trx_id: 當前系統中已經建立過的最新事務(id最大)的id+1的值

當一個事務讀取某條記錄時會追溯undo log版本鏈,找到第一個可以訪問的版本,而該記錄的某一個版本是否能被這個事務讀取到遵循如下規則:(這個規則永遠成立,這個需要好好理解,對後面講解可重複讀和讀已提交兩個級別的實現密切相關)

  1. 如果當前記錄行的row_trx_id小於min_trx_id,表示該版本的記錄在當前事務開啟之前建立,因此可以訪問到

  2. 如果當前記錄行的row_trx_id大於等於max_trx_id,表示該版本的記錄建立晚於當前活躍的事務,因此不能訪問到

  3. 如果當前記錄行的row_trx_id大於等於min_trx_id且小於max_trx_id,則要分兩種情況:

    • 當前記錄行的row_trx_id在m_idx陣列中,則當前事務無法訪問到這個版本的記錄 (除非這個版本的row_trx_id等於當前事務本身的trx_id,本事務當然能訪問自己修改的記錄) ,在m_idx陣列中又不是當前事務自己建立的undo版本,表示是併發訪問的其他事務對這條記錄的修改的結果,則不能訪問到。
    • 當前記錄行的row_trx_id不在m_idx陣列中,則表示這個版本是當前事務開啟之前,其他事務已經提交了的undo版本,當前事務可訪問到。

配合使用read viewundo log版本鏈就能實現事務之間併發訪問相同記錄時,可以根據事務id不同,獲取同一行的不同undo log版本(多版本併發控制)。下面通過模擬併發訪問的兩個事務操作,介紹MVCC的實現(具體來說就是可重複讀讀已提交兩個隔離級別的實現)

可重複讀

下面模擬兩個併發訪問同一條記錄的事務AB的行為,假設這條記錄初始時id=1,a=0,該記錄兩個隱藏欄位row_trx_id = 100,roll_pointer = nil

注意:在可重複讀隔離級別下,當事務sql執行的時候,會生成一個read view快照,且在本事務週期內一直使用這個read view,下面給出了併發訪問同一條記錄的兩個事務AB的具體執行過程,並解釋可重複讀是如何實現的(解決了髒讀不可重複讀)。

image-20220411112017070

事務A的read view:

create_trx_id = 101| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103

事務B的read view:

create_trx_id = 102| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103

(ps. 這裡因為AB事務是併發執行,因此兩個事務建立的read view的max_trx_id = 103)

image-20220410205705912

這裡要注意的是,每次對一條記錄發生修改,就會記錄一個undo log的版本,則在A事務中第二次查詢id=1的記錄的a的值的時候,B事務對該記錄的修改已經新增到版本鏈上了,此時這個undo logtrx_id = 102,在A事務的read viewm_idx陣列中且不等於A事務的trx_id = 101,因此無法訪問到,需要在向前回溯,這裡找到trx_id = 100的記錄版本(小於A事務read viewmin_trx_id屬性,因此可以訪問到),故A事務第二次查詢依舊得到a = 0,而不是B事務修改的a = 1。

你可能有疑問,在A事務第二次查詢的時候,B事務已經完成提交了,那麼A事務的read view的m_idx陣列應該移除102才對啊,它存的不是當前活躍的事務的id嗎?·

注意:在可重複讀隔離級別下,當事務sql執行的時候,會生成一個read view快照,且在本事務週期內一直使用這個read view,雖然102確實應該從A事務的read view中移除,但是因為read view在可重複讀隔離級別下只會在第一條SQL執行時建立一次,並始終保持不變直到事務結束。

那麼也就明白了,在可重複讀隔離級別下,因為read view只在第一條SQL執行時建立,因此併發訪問的其他事務提交前改動的髒資料、以及併發訪問的其他事務提交的改動資料都對當前事務是透明的(儘管確實是記錄在了undo log版本鏈中) ,這就解決了髒讀和不可重複讀(即使其他事務提交的修改,對A事務來說前後查詢結果相同)的問題!

讀已提交

還是藉助上面事務處理的例子,所有的事務處理流程不變,只是將隔離級別調整為讀已提交,讀已提交依舊遵守read view和undo log版本鏈機制,它和可重複讀級別的區別在於,每次執行sql,都會建立一個read view,獲取最新的事務快照。 而因為這個區別,讀已提交產生了不可重複讀的問題,下面來分析一下原因:

image-20220411112017070

事務A第一次查詢建立的read view:

create_trx_id = 101| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103

事務B的read view:

create_trx_id = 102| m_idx = [101, 102]|min_trx_id = 101|max_trx_id = 103

事務A第二次查詢建立的read view:

create_trx_id = 101| m_idx = [101]|min_trx_id = 101|max_trx_id = 103

(ps. 這裡因為AB事務是併發執行,因此兩個事務建立的read view的max_trx_id = 103)

image-20220410205705912

這裡重點觀察A事務的第二次查詢,之前你可能就意識到了,在事務B完成提交後,當前系統中活躍的事務id應該移除102,但是因為在可重複讀隔離級別下,A事務的read view只會在第一個SQL執行時建立,而在讀已提交隔離級別下,每次執行SQL都會建立最新的read view,且此時 m_idx陣列中移除了102,那麼事務A在追溯undo log版本鏈的時候,最新版本記錄的trx_id = 102,102不在A事務的m_idx陣列中,且101 = min_trx_id <= 102 < max_trx_id = 103,因此可以訪問到B事務的提交結果。

那麼對A事務來說,在事務過程中讀取同一條記錄第一次得到a=0,第二次得到a=1,所以出現了不可重複讀的問題(這裡B不提交的話A如果就進行了第二次查詢,則102不會從A事務的read view移除,則A事務依舊訪問不到B事務未提交的修改,因此髒讀還是可以避免的!)

結束語

在我的理解中,MVCC多版本併發控制的實現可以理解成讀已提交、可重複讀兩種隔離級別的實現,通過控制read view的建立時機(其訪問機制是不變的),配合undo log版本鏈可以實現事務之間對同一條記錄的併發訪問,並獲得不同的結果。

關注公眾號【程式設計師白澤】,帶你走進一個不一樣的程式設計師/學生黨,公眾號回覆【簡歷】可以獲得我正在使用的簡歷模板,平時也會同步更新文章。希望大家都能收穫心儀的offer~

相關文章