一文搞懂MySQL事務的隔離性如何實現|MVCC
關注公眾號【程式設計師白澤】,帶你走進一個不一樣的程式設計師/學生黨
前言
MySQL有ACID四大特性,本文著重講解MySQL不同事務之間的隔離性的概念,以及MySQL如何實現隔離性。下面先羅列一下MySQL的四種事務隔離級別,以及不同隔離級別可能會存在的問題。事務隔離級別越高,多個事務在併發訪問資料庫時互相產生資料干擾的可能性越低,但是併發訪問的效能就越差。(相當於犧牲了一定的效能去保證資料的安全性)
下面這張表,展示了MySQL的四大隔離級別和伴隨著的一些問題,下面詳細介紹。
事務隔離級別
讀未提交:多個事務同時修改一條記錄,A事務對其的改動在A事務還沒提交時,在B事務中就可以看到A事務對其的改動。
讀已提交:多個事務同時修改一條記錄,A事務對其的改動在A事務提交之後,在B事務中可以看到A事務對其的改動。
可重複讀:多個事務同時修改一條記錄,這條記錄在A事務執行期間是不變的(別的事務對這條記錄的修改不被A事務感知)。
序列化:多個事務同時訪問一條記錄(CRUD),讀加讀鎖,寫加寫鎖,完全退化成了序列的訪問,自然不會收到任何其他事務的干擾,效能最低。
不同級別伴隨的問題
髒讀:A事務在提交前對一個欄位的改動會被B事務感知,那麼事務之間就很容易產生干擾,假如A對一個欄位改動之後被B感知,但是A又回滾了事務,則對該欄位的改動依舊保留在B的查詢結果中,那麼這樣的資料就是髒資料(處於處理中間過程的資料)。
不可重複讀:A事務對於一條記錄的讀取結果,在B事務對其修改並提交之後,A再次讀取同一條記錄會得到不同的結果。
幻讀:側重於A事務的同一個範圍查詢命令,前後兩次得到不同的記錄數量,原因是B事務可能對其進行了插入。
小結一下
通過閱讀上面給出的內容,可以得到結論:
- 讀未提交隔離級別並沒有對行資料的可見性做任何限制,所有事務之間的改動都是互相可見的,所以存在很多問題,不推薦使用;
- 序列化隔離級別因為通過鎖機制對記錄的訪問進行限制,所以安全性最高,但併發訪問退化成序列訪問,效能較低;
因此本文將側重於探究MySQL如何實現讀已提交
和可重複讀
兩種隔離級別(也就是你聽聞的MVCC多版本併發控制的實現),通過後面的學習你將理解讀已提交
解決髒讀
,可重複讀
隔離級別如何更進一步解決不可重複讀
。
接下來我將向你介紹undo 版本鏈
機制以及read view
快照讀機制,這兩個機制相互配合是實現MVCC的核心,而讀已提交
和可重複讀
隔離級別的實現都是建立在這兩個核心機制之上。
undo 版本鏈
undo 版本鏈就是指undo log的儲存在邏輯上的表現形式,它被用於事務當中的回滾操作以及實現MVCC,這裡介紹一下undo log之所以能實現回滾記錄的原理。
對於每一行記錄,會有兩個隱藏欄位:row_trx_id
和roll_pointer
,row_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版本鏈
記錄如下:
在上圖中,最下方的undo log中記錄了當前行的最新版本,而該條記錄之前的版本則以版本鏈的形式可追溯,這也是事務回滾所做的事。那undo log版本鏈和事務的隔離性有什麼關係呢?那就要引入另一個核心機制:read view。
read view
read view表示快照讀,這個快照讀會記錄四個關鍵的屬性:
-
create_trx_id
: 當前事務的id -
m_idx
: 當前正在活躍的所有事務id(id陣列),沒有提交的事務的id -
min_trx_id
: 當前系統中活躍的事務的id最小值 -
max_trx_id
: 當前系統中已經建立過的最新事務(id最大)的id+1的值
當一個事務讀取某條記錄時會追溯undo log版本鏈,找到第一個可以訪問的版本,而該記錄的某一個版本是否能被這個事務讀取到遵循如下規則:(這個規則永遠成立,這個需要好好理解,對後面講解可重複讀和讀已提交兩個級別的實現密切相關)
-
如果當前記錄行的row_trx_id小於min_trx_id,表示該版本的記錄在當前事務開啟之前建立,因此可以訪問到
-
如果當前記錄行的row_trx_id大於等於max_trx_id,表示該版本的記錄建立晚於當前活躍的事務,因此不能訪問到
-
如果當前記錄行的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 view
和undo log版本鏈
就能實現事務之間併發訪問
相同記錄時,可以根據事務id不同,獲取同一行的不同undo log版本(多版本併發控制)。下面通過模擬併發訪問的兩個事務操作,介紹MVCC的實現(具體來說就是可重複讀和讀已提交兩個隔離級別的實現)
可重複讀
下面模擬兩個併發訪問同一條記錄的事務AB的行為,假設這條記錄初始時id=1,a=0,該記錄兩個隱藏欄位row_trx_id = 100,roll_pointer = nil
注意:在可重複讀隔離級別下,當事務sql執行的時候,會生成一個read view快照,且在本事務週期內一直使用這個read view,下面給出了併發訪問同一條記錄的兩個事務AB的具體執行過程,並解釋可重複讀
是如何實現的(解決了髒讀
和不可重複讀
)。
事務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)
這裡要注意的是,每次對一條記錄發生修改,就會記錄一個undo log的版本,則在A事務中第二次查詢id=1的記錄的a的值的時候,B事務對該記錄的修改已經新增到版本鏈上了,此時這個undo log
的trx_id = 102
,在A事務的read view
的m_idx陣列
中且不等於A事務的trx_id = 101
,因此無法訪問到,需要在向前回溯,這裡找到trx_id = 100
的記錄版本(小於A事務read view
的min_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,獲取最新的事務快照。 而因為這個區別,讀已提交產生了不可重複讀的問題,下面來分析一下原因:
事務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)
這裡重點觀察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~