## 前言
MySQL有ACID四大特性必怜,本文著重講解**MySQL不同事務(wù)之間的隔離性**的概念,以及MySQL如何實(shí)現(xiàn)隔離性后频。下面先羅列一下MySQL的四種事務(wù)隔離級別棚赔,以及不同隔離級別可能會(huì)存在的問題。**事務(wù)隔離級別越高徘郭,多個(gè)事務(wù)在并發(fā)訪問數(shù)據(jù)庫時(shí)互相產(chǎn)生數(shù)據(jù)干擾的可能性越低,但是并發(fā)訪問的性能就越差**丧肴。(相當(dāng)于犧牲了一定的性能去保證數(shù)據(jù)的安全性)
下面這張表残揉,展示了MySQL的四大隔離級別和伴隨著的一些問題,下面詳細(xì)介紹芋浮。

## 事務(wù)隔離級別
讀未提交:多個(gè)事務(wù)同時(shí)修改一條記錄抱环,A事務(wù)對其的改動(dòng)在A事務(wù)還沒提交時(shí)壳快,在B事務(wù)中就可以看到A事務(wù)對其的改動(dòng)。
讀已提交:多個(gè)事務(wù)同時(shí)修改一條記錄镇草,A事務(wù)對其的改動(dòng)在A事務(wù)提交之后眶痰,在B事務(wù)中可以看到A事務(wù)對其的改動(dòng)。
可重復(fù)讀:多個(gè)事務(wù)同時(shí)修改一條記錄梯啤,這條記錄在A事務(wù)執(zhí)行期間是不變的(別的事務(wù)對這條記錄的修改不被A事務(wù)感知)竖伯。
串行化:多個(gè)事務(wù)同時(shí)訪問一條記錄(CRUD),讀加讀鎖因宇,寫加寫鎖,完全退化成了串行的訪問察滑,自然不會(huì)收到任何其他事務(wù)的干擾,性能最低户盯。
## 不同級別伴隨的問題
臟讀:A事務(wù)在提交前對一個(gè)字段的改動(dòng)會(huì)被B事務(wù)感知,那么事務(wù)之間就很容易產(chǎn)生干擾饲化,假如A對一個(gè)字段改動(dòng)之后被B感知莽鸭,但是A又回滾了事務(wù)滓侍,則對該字段的改動(dòng)依舊保留在B的查詢結(jié)果中,那么這樣的數(shù)據(jù)就是臟數(shù)據(jù)(處于處理中間過程的數(shù)據(jù))撩笆。
不可重復(fù)讀:A事務(wù)對于一條記錄的讀取結(jié)果捺球,在B事務(wù)對其修改并提交之后,A再次讀取同一條記錄會(huì)得到不同的結(jié)果夕冲。
幻讀:側(cè)重于A事務(wù)的同一個(gè)范圍查詢命令,前后兩次得到不同的記錄數(shù)量泣栈,原因是B事務(wù)可能對其進(jìn)行了插入弥姻。
### 小結(jié)一下
通過閱讀上面給出的內(nèi)容,可以得到結(jié)論:
1.? 讀未提交隔離級別并沒有對行數(shù)據(jù)的可見性做任何限制疼进,所有事務(wù)之間的改動(dòng)都是互相可見的,所以存在很多問題伞广,不推薦使用;
2.? 串行化隔離級別因?yàn)橥ㄟ^鎖機(jī)制對記錄的訪問進(jìn)行限制减拭,所以安全性最高区丑,但并發(fā)訪問退化成串行訪問,性能較低既们;
**因此本文將側(cè)重于探究MySQL如何實(shí)現(xiàn)`讀已提交`和`可重復(fù)讀`兩種隔離級別(也就是你聽聞的MVCC多版本并發(fā)控制的實(shí)現(xiàn))正什,通過后面的學(xué)習(xí)你將理解`讀已提交`隔離級別如何`解決臟讀`,`可重復(fù)讀`隔離級別如何更進(jìn)一步`解決不可重復(fù)讀`斯棒。**
**接下來我將向你介紹`undo 版本鏈`機(jī)制以及`read view`快照讀機(jī)制主经,這兩個(gè)機(jī)制相互配合是實(shí)現(xiàn)MVCC的核心,而`讀已提交`和`可重復(fù)讀`隔離級別的實(shí)現(xiàn)都是建立在這兩個(gè)核心機(jī)制之上穗酥。**
## undo 版本鏈
undo 版本鏈就是指undo log的存儲(chǔ)在邏輯上的表現(xiàn)形式惠遏,它被用于事務(wù)當(dāng)中的**回滾操作**以及**實(shí)現(xiàn)MVCC**,這里介紹一下undo log之所以能實(shí)現(xiàn)回滾記錄的原理抽高。
對于每一行記錄透绩,會(huì)有兩個(gè)隱藏字段:`row_trx_id`和`roll_pointer`,`row_trx_id`表示更新(改動(dòng))本條記錄的全局事務(wù)id?**(每個(gè)事務(wù)創(chuàng)建都會(huì)分配id碳竟,全局遞增狸臣,因此事務(wù)id區(qū)別對某條記錄的修改是由哪個(gè)事務(wù)作出的)**?,`roll_pointer`是回滾指針统翩,指向當(dāng)前記錄的前一個(gè)`undo log版本`此洲,如果是第一個(gè)版本則`roll_pointer`指向nil,這樣如果有多個(gè)事務(wù)對同一條記錄進(jìn)行了多次改動(dòng)娶桦,則會(huì)在`undo log`中以鏈的形式存儲(chǔ)改動(dòng)過程汁汗。
假如有兩個(gè)事務(wù)AB,數(shù)據(jù)表中有一行id為1的記錄祈争,其字段a初始值為0角寸,事務(wù)A對id=1的行的a修改為1,事務(wù)B對id=1的行的a字段修改為2沮峡,則`undo log版本鏈`記錄如下:

在上圖中亿柑,最下方的undo log中記錄了當(dāng)前行的最新版本,而該條記錄之前的版本則以版本鏈的形式可追溯疟游,這也是事務(wù)回滾所做的事式矫。那undo log版本鏈和事務(wù)的隔離性有什么關(guān)系呢?**那就要引入另一個(gè)核心機(jī)制:read view聪廉。**
## read view
read view表示快照讀故慈,這個(gè)快照讀會(huì)記錄四個(gè)關(guān)鍵的屬性:
1.? `create_trx_id`: 當(dāng)前事務(wù)的id
2.? `m_idx`: 當(dāng)前正在活躍的所有事務(wù)id(id數(shù)組),沒有提交的事務(wù)的id
3.? `min_trx_id`: 當(dāng)前系統(tǒng)中活躍的事務(wù)的id最小值
4.? `max_trx_id`: 當(dāng)前系統(tǒng)中已經(jīng)創(chuàng)建過的最新事務(wù)(id最大)的id+1的值
**當(dāng)一個(gè)事務(wù)讀取某條記錄時(shí)會(huì)追溯undo log版本鏈干签,找到第一個(gè)可以訪問的版本拆撼,而該記錄的某一個(gè)版本是否能被這個(gè)事務(wù)讀取到遵循如下規(guī)則:(這個(gè)規(guī)則永遠(yuǎn)成立喘沿,這個(gè)需要好好理解蚜印,對后面講解可重復(fù)讀和讀已提交兩個(gè)級別的實(shí)現(xiàn)密切相關(guān))**
1.? 如果當(dāng)前記錄行的row_trx_id小于min_trx_id留量,表示該版本的記錄在當(dāng)前事務(wù)開啟之前創(chuàng)建,因此可以訪問到
2.? 如果當(dāng)前記錄行的row_trx_id大于等于max_trx_id忆绰,表示該版本的記錄創(chuàng)建晚于當(dāng)前活躍的事務(wù)可岂,因此不能訪問到
3.? 如果當(dāng)前記錄行的row_trx_id大于等于min_trx_id且小于max_trx_id,則要分兩種情況:
? ? *? 當(dāng)前記錄行的row_trx_id在m_idx數(shù)組中伐债,則當(dāng)前事務(wù)無法訪問到這個(gè)版本的記錄?**(除非這個(gè)版本的row_trx_id等于當(dāng)前事務(wù)本身的trx_id致开,本事務(wù)當(dāng)然能訪問自己修改的記錄)**?,在m_idx數(shù)組中又不是當(dāng)前事務(wù)自己創(chuàng)建的undo版本虹蒋,表示是并發(fā)訪問的其他事務(wù)對這條記錄的修改的結(jié)果飒货,則不能訪問到。
? ? *? 當(dāng)前記錄行的row_trx_id不在m_idx數(shù)組中晃虫,則表示這個(gè)版本是當(dāng)前事務(wù)開啟之前扣墩,其他事務(wù)已經(jīng)提交了的undo版本,當(dāng)前事務(wù)可訪問到荆责。
配合使用`read view`和`undo log版本鏈`就能實(shí)現(xiàn)**事務(wù)之間`并發(fā)訪問`相同記錄**時(shí)亚脆,可以根據(jù)事務(wù)id不同,獲取同一行的不同undo log版本(多版本并發(fā)控制)键耕。**下面通過模擬并發(fā)訪問的兩個(gè)事務(wù)操作**,介紹MVCC的實(shí)現(xiàn)(具體來說就是**可重復(fù)讀**和**讀已提交**兩個(gè)隔離級別的實(shí)現(xiàn))
### 可重復(fù)讀
下面模擬兩個(gè)并發(fā)訪問同一條記錄的事務(wù)AB的行為玛迄,假設(shè)這條記錄初始時(shí)id=1棚亩,a=0虏杰,該記錄兩個(gè)隱藏字段row_trx_id = 100,roll_pointer = nil
**注意:在可重復(fù)讀隔離級別下瘸彤,當(dāng)事務(wù)sql執(zhí)行的時(shí)候笛钝,會(huì)生成一個(gè)read view快照玻靡,且在本事務(wù)周期內(nèi)一直使用這個(gè)read view**,下面給出了并發(fā)訪問同一條記錄的兩個(gè)事務(wù)AB的具體執(zhí)行過程囤捻,并解釋`可重復(fù)讀`是如何實(shí)現(xiàn)的(解決了`臟讀`和`不可重復(fù)讀`)蝎土。

事務(wù)A的read view:
`create_trx_id`?= 101|?`m_idx`?= [101, 102]|`min_trx_id`?= 101|`max_trx_id`?= 103
事務(wù)B的read view:
`create_trx_id`?= 102|?`m_idx`?= [101, 102]|`min_trx_id`?= 101|`max_trx_id`?= 103
(ps. 這里因?yàn)锳B事務(wù)是并發(fā)執(zhí)行,因此兩個(gè)事務(wù)創(chuàng)建的read view的max_trx_id = 103)

**這里要注意的是挡毅,每次對一條記錄發(fā)生修改暴构,就會(huì)記錄一個(gè)undo log的版本**,則在A事務(wù)中第二次查詢id=1的記錄的a的值的時(shí)候庆械,B事務(wù)對該記錄的修改已經(jīng)添加到版本鏈上了菌赖,此時(shí)這個(gè)`undo log`的`trx_id = 102`,在A事務(wù)的`read view`的`m_idx數(shù)組`中且不等于A事務(wù)的`trx_id = 101`堕绩,因此無法訪問到,需要在向前回溯特姐,這里找到`trx_id = 100`的記錄版本(小于A事務(wù)`read view`的`min_trx_id`屬性黍氮,因此可以訪問到),故A事務(wù)第二次查詢依舊得到a = 0捷枯,而不是B事務(wù)修改的a = 1专执。
你可能有疑問,在A事務(wù)第二次查詢的時(shí)候攀痊,B事務(wù)已經(jīng)完成提交了拄显,那么A事務(wù)的read view的m_idx數(shù)組應(yīng)該移除102才對啊,它存的不是當(dāng)前活躍的事務(wù)的id嗎涩笤?·
**注意:在可重復(fù)讀隔離級別下盒件,當(dāng)事務(wù)sql執(zhí)行的時(shí)候,會(huì)生成一個(gè)read view快照恩沽,且在本事務(wù)周期內(nèi)一直使用這個(gè)read view**翔始,雖然102確實(shí)應(yīng)該從A事務(wù)的read view中移除城瞎,但是因?yàn)閞ead view在可重復(fù)讀隔離級別下只會(huì)在第一條SQL執(zhí)行時(shí)創(chuàng)建一次,并始終保持不變直到事務(wù)結(jié)束脖镀。
**那么也就明白了步势,在可重復(fù)讀隔離級別下敛腌,因?yàn)閞ead view只在第一條SQL執(zhí)行時(shí)創(chuàng)建,因此并發(fā)訪問的其他事務(wù)提交前改動(dòng)的臟數(shù)據(jù)削祈、以及并發(fā)訪問的其他事務(wù)提交的改動(dòng)數(shù)據(jù)都對當(dāng)前事務(wù)是透明的(盡管確實(shí)是記錄在了undo log版本鏈中)**?脑漫,這就解決了臟讀和不可重復(fù)讀(即使其他事務(wù)提交的修改,對A事務(wù)來說前后查詢結(jié)果相同)的問題启昧!
### 讀已提交
還是借助上面事務(wù)處理的例子劈伴,所有的事務(wù)處理流程不變握爷,**只是將隔離級別調(diào)整為讀已提交新啼,讀已提交依舊遵守read view和undo log版本鏈機(jī)制,它和可重復(fù)讀級別的區(qū)別在于燥撞,每次執(zhí)行sql物舒,都會(huì)創(chuàng)建一個(gè)read view,獲取最新的事務(wù)快照火诸。**?而因?yàn)檫@個(gè)區(qū)別荠察,讀已提交產(chǎn)生了不可重復(fù)讀的問題,下面來分析一下原因:

事務(wù)A第一次查詢創(chuàng)建的read view:
`create_trx_id`?= 101|?`m_idx`?= [101, 102]|`min_trx_id`?= 101|`max_trx_id`?= 103
事務(wù)B的read view:
`create_trx_id`?= 102|?`m_idx`?= [101, 102]|`min_trx_id`?= 101|`max_trx_id`?= 103
事務(wù)A第二次查詢創(chuàng)建的read view:
`create_trx_id`?= 101|?`m_idx`?= [101]|`min_trx_id`?= 101|`max_trx_id`?= 103
(ps. 這里因?yàn)锳B事務(wù)是并發(fā)執(zhí)行盯荤,因此兩個(gè)事務(wù)創(chuàng)建的read view的max_trx_id = 103)

這里重點(diǎn)觀察A事務(wù)的第二次查詢秋秤,之前你可能就意識(shí)到了,在事務(wù)B完成提交后航缀,當(dāng)前系統(tǒng)中活躍的事務(wù)id應(yīng)該移除102芥玉,但是因?yàn)?*在可重復(fù)讀隔離級別下,A事務(wù)的`read view`只會(huì)在第一個(gè)SQL執(zhí)行時(shí)創(chuàng)建赶袄,而在讀已提交隔離級別下抠藕,每次執(zhí)行SQL都會(huì)創(chuàng)建最新的read view**,且此時(shí)?`m_idx`數(shù)組中移除了102敬辣,那么事務(wù)A在追溯undo log版本鏈的時(shí)候零院,最新版本記錄的`trx_id = 102`,102不在A事務(wù)的m_idx數(shù)組中撰茎,且`101 = min_trx_id <= 102 < max_trx_id = 103`打洼,因此可以訪問到B事務(wù)的提交結(jié)果。
**那么對A事務(wù)來說炫惩,在事務(wù)過程中讀取同一條記錄第一次得到a=0阿浓,第二次得到a=1搔扁,所以出現(xiàn)了不可重復(fù)讀的問題(這里B不提交的話A如果就進(jìn)行了第二次查詢,則102不會(huì)從A事務(wù)的read view移除扭勉,則A事務(wù)依舊訪問不到B事務(wù)未提交的修改苛聘,因此臟讀還是可以避免的V揖邸)**
## 結(jié)束語
在我的理解中唱捣,MVCC多版本并發(fā)控制的實(shí)現(xiàn)可以理解成讀已提交震缭、可重復(fù)讀兩種隔離級別的實(shí)現(xiàn),通過控制read view的創(chuàng)建時(shí)機(jī)(其訪問機(jī)制是不變的)党涕,配合undo log版本鏈可以實(shí)現(xiàn)事務(wù)之間對同一條記錄的并發(fā)訪問巡社,并獲得不同的結(jié)果。