淺聊MVCC?你學(xué)會(huì)了嗎?
MVCC,即多版本并發(fā)控制(Multiversion Concurrency Control),類似于數(shù)據(jù)庫鎖,是一種優(yōu)雅的并發(fā)控制方案。
我們了解,在數(shù)據(jù)庫環(huán)境中,數(shù)據(jù)操作主要包括讀取和寫入兩種操作,在并發(fā)情境下,可能出現(xiàn)以下三種情況:
- 讀-讀并發(fā)
- 讀-寫并發(fā)
- 寫-寫并發(fā)
眾所周知,在讀取操作時(shí)沒有寫入操作的情況下,并發(fā)讀取不會(huì)引發(fā)問題;而寫入操作并發(fā)時(shí),常常會(huì)通過加鎖的方式來處理。而針對讀取-寫入并發(fā)的場景,則可通過MVCC機(jī)制來解決。
快照讀和當(dāng)前讀
要深入了解MVCC機(jī)制,其中最關(guān)鍵的一個(gè)概念就是快照讀。
所謂快照讀,即讀取快照數(shù)據(jù),即在生成快照時(shí)刻的數(shù)據(jù)。比如我們常用的普通SELECT語句在無鎖情況下就屬于快照讀。例如:
SELECT * FROM xx_table WHERE ...
與快照讀相對應(yīng)的另一個(gè)概念是當(dāng)前讀,當(dāng)前讀即獲取最新的數(shù)據(jù)。因此,加鎖的SELECT操作或進(jìn)行數(shù)據(jù)的增刪改都屬于當(dāng)前讀。例如:
SELECT * FROM xx_table LOCK IN SHARE MODE;
SELECT * FROM xx_table FOR UPDATE;
INSERT INTO xx_table ...
DELETE FROM xx_table ...
UPDATE xx_table ...
可以理解為:快照讀是MVCC實(shí)現(xiàn)的基礎(chǔ),而當(dāng)前讀則是悲觀鎖實(shí)現(xiàn)的基礎(chǔ)。
快照讀所讀取的快照數(shù)據(jù)來自于何處?換言之,這些快照數(shù)據(jù)存儲(chǔ)在何處?
UndoLog
undo log是 MySQL 中一種重要的事務(wù)日志之一。顧名思義,undo log 是用于回滾操作的日志。在事務(wù)提交之前,MySQL會(huì)將更新前的數(shù)據(jù)記錄到 undo log 日志文件中。當(dāng)需要回滾事務(wù)或者發(fā)生數(shù)據(jù)庫崩潰時(shí),可以通過 undo log 進(jìn)行數(shù)據(jù)回退。
在這個(gè)過程中提到的 "更新前的數(shù)據(jù)" 存儲(chǔ)在undo log中,即我們之前提及的快照。因此,這正是許多人認(rèn)為 Undo Log 是實(shí)現(xiàn) MVCC 的重要工具的原因之一。
在同一時(shí)刻,一條記錄可能會(huì)被多個(gè)事務(wù)操作。因此,undo log 可能會(huì)包含一條記錄的多個(gè)快照。當(dāng)需要進(jìn)行快照讀取時(shí),就要考慮應(yīng)該讀取哪個(gè)快照。這時(shí)候就需要利用其他相關(guān)信息來做出決定。
行記錄的隱式字段
實(shí)際上,在數(shù)據(jù)庫的每一行記錄中,除了保存我們自定義的字段之外,還包含一些重要的隱式字段:
- db_row_id:隱式主鍵。如果表沒有創(chuàng)建主鍵,將使用該字段創(chuàng)建聚簇索引。
- db_trx_id:最后一次修改該記錄的事務(wù)ID。
- db_roll_ptr:回滾指針,指向記錄的上一個(gè)版本,在本質(zhì)上指向Undo Log中的前一個(gè)版本的快照地址。
由于每次記錄更改之前都會(huì)先將一個(gè)快照存儲(chǔ)到undo log中,這些隱式字段也會(huì)與記錄一起保存在undo log中。因此,每個(gè)快照中都包含一個(gè)db_trx_id字段,表示最后一次修改該記錄的事務(wù)ID,以及一個(gè)db_roll_ptr字段,指向前一個(gè)快照的地址。(db_trx_id和db_roll_ptr是重點(diǎn),將在后續(xù)中用到)
因此,這樣就形成了一個(gè)快照鏈表:
圖片
有了undo log,又有了幾個(gè)隱式字段,我們好像還是不知道具體應(yīng)該讀取哪個(gè)快照,那怎么辦呢?
Read View
此時(shí),Read View 登場,它的主要作用是解決可見性問題,即確定當(dāng)前事務(wù)應(yīng)該查看哪個(gè)快照,而不應(yīng)查看哪個(gè)快照。
在 Read View 中具有幾個(gè)重要屬性:
- trx_ids:系統(tǒng)當(dāng)前未提交的事務(wù)ID列表。
- low_limit_id:應(yīng)分配給下一個(gè)事務(wù)的ID值。
- up_limit_id:未提交事務(wù)中最小的事務(wù)ID。
- creator_trx_id:創(chuàng)建該 Read View 的事務(wù)ID。
每次啟動(dòng)一個(gè)事務(wù),都會(huì)獲得一個(gè)遞增的事務(wù)ID。通過ID的大小,我們可以確定事務(wù)的時(shí)間順序。
其實(shí)原則比較簡單,那就是事務(wù)ID大的事務(wù)應(yīng)該能看到事務(wù)ID小的事務(wù)的變更結(jié)果,反之則不能!舉個(gè)例子:
假設(shè)當(dāng)前存在一個(gè)事務(wù)3想要進(jìn)行快照讀取某條記錄,它會(huì)首先創(chuàng)建一個(gè)Read View,并記錄所有當(dāng)前未提交事務(wù)的信息。例如,up_limit_id = 2,low_limit_id = 5,trx_ids= [2,4,5],creator_trx_id= 3
圖片
前文提到,每條記錄都包含一個(gè)隱式字段db_trx_id,記錄對該記錄進(jìn)行最新修改的事務(wù)ID,例如db_trx_id = 3;
接下來,數(shù)據(jù)庫將檢查此記錄的db_trx_id與Read View進(jìn)行可見性比較。
- 若db_trx_id < up_limit_id,則意味著在Read View中所有未提交事務(wù)創(chuàng)建之前,事務(wù)ID為3的操作已經(jīng)提交,并在此期間沒有新的提交。因此,對當(dāng)前事務(wù)而言,此記錄應(yīng)該是可見的。
- 若db_trx_id > low_limit_id,則表示事務(wù)ID為3的操作是在Read View中所有未提交事務(wù)創(chuàng)建之后才提交的,也就是在當(dāng)前事務(wù)開啟之后,有其他事務(wù)修改了數(shù)據(jù)并提交。因此,這條記錄對當(dāng)前事務(wù)來說是不可見的。(不可見時(shí)的處理將在后文討論)
另一種情況是,up_limit_id < db_trx_id < low_limit_id。在此情況下,將db_trx_id與Read View中的trx_ids逐一比較。
- 若db_trx_id在trx_ids列表中,表示在當(dāng)前事務(wù)開啟時(shí),某些未提交事務(wù)對數(shù)據(jù)進(jìn)行了更改并提交,因此,對當(dāng)前事務(wù)來說,此記錄應(yīng)該是不可見的。
- 若db_trx_id不在trx_ids列表中,表示在當(dāng)前事務(wù)開啟之前,其他事務(wù)對數(shù)據(jù)進(jìn)行了修改并提交,所以對當(dāng)前事務(wù)來說,該記錄是可見的。
因此,在讀取記錄時(shí),經(jīng)過上述判斷,若記錄對當(dāng)前事務(wù)可見,則直接返回。若不可見,則需要利用undo log。
當(dāng)數(shù)據(jù)的事務(wù)ID與Read View規(guī)則不符時(shí),需要從undo log中獲取數(shù)據(jù)的歷史快照,然后使用數(shù)據(jù)快照的事務(wù)ID與Read View進(jìn)行可見性比較。如果找到一條快照,則返回?cái)?shù)據(jù);否則,返回空。
圖片
因此,在InnoDB中,MVCC機(jī)制通過Read View和Undo Log相結(jié)合來實(shí)現(xiàn)。Undo Log保存了歷史快照,而Read View則確定了哪一個(gè)具體的快照對當(dāng)前操作是可見的。
MVCC和可重復(fù)讀
根據(jù)不同的事務(wù)隔離級別,在InnoDB中,獲取Read View的時(shí)機(jī)有所不同。在可重復(fù)讀隔離級別下,每次查詢都會(huì)重新獲取一次Read View,而在讀已提交隔離級別下,只有在事務(wù)的第一次查詢時(shí)獲取一次Read View。
因此,在可重復(fù)讀隔離級別下,由于MVCC機(jī)制的存在,能夠有效解決不可重復(fù)讀的問題。因?yàn)樵诳芍貜?fù)讀隔離級別中,只在第一次查詢時(shí)獲取一次Read View,從而天然消除了可能導(dǎo)致重讀問題的可能性。