新聞中心
大家好,我是小林。

創(chuàng)新互聯(lián)建站于2013年開(kāi)始,先為麗水等服務(wù)建站,麗水等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢(xún)服務(wù)。為麗水企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。
之前寫(xiě)過(guò)一篇 MySQL 的 MVCC 的工作原理,最近有讀者在網(wǎng)站上學(xué)習(xí)的時(shí)候,評(píng)論區(qū)指出了一些問(wèn)題。
而這個(gè)知識(shí)點(diǎn)很重要,面試太常問(wèn)了,所以,我就重寫(xiě)了這篇文章!開(kāi)車(chē)!
正文
這是我的錢(qián)包,共有 100 萬(wàn)元。
今天我心情好,我決定給你的轉(zhuǎn)賬 100 萬(wàn),最后的結(jié)果肯定是我的余額變?yōu)?0 元,你的余額多了 100 萬(wàn)元,是不是想到就很開(kāi)心?
轉(zhuǎn)賬這一動(dòng)作在程序里會(huì)涉及到一系列的操作,假設(shè)我向你轉(zhuǎn)賬 100 萬(wàn)的過(guò)程是有下面這幾個(gè)步驟組成的:
可以看到這個(gè)轉(zhuǎn)賬的過(guò)程涉及到了兩次修改數(shù)據(jù)庫(kù)的操作。
假設(shè)在執(zhí)行第三步驟之后,服務(wù)器忽然掉電了,就會(huì)發(fā)生一個(gè)蛋疼的事情,我的賬戶(hù)扣了 100 萬(wàn),但是錢(qián)并沒(méi)有到你的賬戶(hù)上,也就是說(shuō)這 100 萬(wàn)消失了!
要解決這個(gè)問(wèn)題,就要保證轉(zhuǎn)賬業(yè)務(wù)里的所有數(shù)據(jù)庫(kù)的操作是不可分割的,要么全部執(zhí)行成功 ,要么全部失敗,不允許出現(xiàn)中間狀態(tài)的數(shù)據(jù)。
數(shù)據(jù)庫(kù)中的「事務(wù)(Transaction)」就能達(dá)到這樣的效果。
我們?cè)谵D(zhuǎn)賬操作前先開(kāi)啟事務(wù),等所有數(shù)據(jù)庫(kù)操作執(zhí)行完成后,才提交事務(wù),對(duì)于已經(jīng)提交的事務(wù)來(lái)說(shuō),該事務(wù)對(duì)數(shù)據(jù)庫(kù)所做的修改將永久生效,如果中途發(fā)生發(fā)生中斷或錯(cuò)誤,那么該事務(wù)期間對(duì)數(shù)據(jù)庫(kù)所做的修改將會(huì)被回滾到?jīng)]執(zhí)行該事務(wù)之前的狀態(tài)。
事務(wù)有哪些特性?
事務(wù)是由 MySQL 的引擎來(lái)實(shí)現(xiàn)的,我們常見(jiàn)的 InnoDB 引擎它是支持事務(wù)的。
不過(guò)并不是所有的引擎都能支持事務(wù),比如 MySQL 原生的 MyISAM 引擎就不支持事務(wù),也正是這樣,所以大多數(shù) MySQL 的引擎都是用 InnoDB。
事務(wù)看起來(lái)感覺(jué)簡(jiǎn)單,但是要實(shí)現(xiàn)事務(wù)必須要遵守 4 個(gè)特性,分別如下:
- 原子性(Atomicity):一個(gè)事務(wù)中的所有操作,要么全部完成,要么全部不完成,不會(huì)結(jié)束在中間某個(gè)環(huán)節(jié),而且事務(wù)在執(zhí)行過(guò)程中發(fā)生錯(cuò)誤,會(huì)被回滾到事務(wù)開(kāi)始前的狀態(tài),就像這個(gè)事務(wù)從來(lái)沒(méi)有執(zhí)行過(guò)一樣;
- 一致性(Consistency):數(shù)據(jù)庫(kù)的完整性不會(huì)因?yàn)槭聞?wù)的執(zhí)行而受到破壞,比如表中有一個(gè)字段為姓名,它有唯一約束,也就是表中姓名不能重復(fù),如果一個(gè)事務(wù)對(duì)姓名字段進(jìn)行了修改,但是在事務(wù)提交后,表中的姓名變得非唯一性了,這就破壞了事務(wù)的一致性要求,這時(shí)數(shù)據(jù)庫(kù)就要撤銷(xiāo)該事務(wù),返回初始化的狀態(tài)。
- 隔離性(Isolation):數(shù)據(jù)庫(kù)允許多個(gè)并發(fā)事務(wù)同時(shí)對(duì)其數(shù)據(jù)進(jìn)行讀寫(xiě)和修改的能力,隔離性可以防止多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)由于交叉執(zhí)行而導(dǎo)致數(shù)據(jù)的不一致。
- 持久性(Durability):事務(wù)處理結(jié)束后,對(duì)數(shù)據(jù)的修改就是永久的,即便系統(tǒng)故障也不會(huì)丟失。
InnoDB 引擎通過(guò)什么技術(shù)來(lái)保證事務(wù)的這四個(gè)特性的呢?
- 持久性是通過(guò) redo log (重做日志)來(lái)保證的;
- 原子性是通過(guò) undo log(回滾日志) 來(lái)保證的;
- 隔離性是通過(guò) MVCC(多版本并發(fā)控制) 或鎖機(jī)制來(lái)保證的;
- 一致性則是通過(guò)持久性+原子性+隔離性來(lái)保證;
這次將重點(diǎn)介紹事務(wù)的隔離性,這也是面試時(shí)最常問(wèn)的知識(shí)的點(diǎn)。
為什么事務(wù)要有隔離性,我們就要知道并發(fā)事務(wù)時(shí)會(huì)引發(fā)什么問(wèn)題。
并行事務(wù)會(huì)引發(fā)什么問(wèn)題?
MySQL 服務(wù)端是允許多個(gè)客戶(hù)端連接的,這意味著 MySQL 會(huì)出現(xiàn)同時(shí)處理多個(gè)事務(wù)的情況。
那么在同時(shí)處理多個(gè)事務(wù)的時(shí)候,就可能出現(xiàn)臟讀(dirty read)、不可重復(fù)讀(non-repeatable read)、幻讀(phantom read)的問(wèn)題。
接下來(lái),通過(guò)舉例子給大家說(shuō)明,這些問(wèn)題是如何發(fā)生的。
臟讀
如果一個(gè)事務(wù)「讀到」了另一個(gè)「未提交事務(wù)修改過(guò)的數(shù)據(jù)」,就意味著發(fā)生了「臟讀」現(xiàn)象。
舉個(gè)栗子。
假設(shè)有 A 和 B 這兩個(gè)事務(wù)同時(shí)在處理,事務(wù) A 先開(kāi)始從數(shù)據(jù)庫(kù)中讀取小林的余額數(shù)據(jù),然后再執(zhí)行更新操作,如果此時(shí)事務(wù) A 還沒(méi)有提交事務(wù),而此時(shí)正好事務(wù) B 也從數(shù)據(jù)庫(kù)中讀取小林的余額數(shù)據(jù),那么事務(wù) B 讀取到的余額數(shù)據(jù)是剛才事務(wù) A 更新后的數(shù)據(jù),即使沒(méi)有提交事務(wù)。
因?yàn)槭聞?wù) A 是還沒(méi)提交事務(wù)的,也就是它隨時(shí)可能發(fā)生回滾操作,如果在上面這種情況事務(wù) A 發(fā)生了回滾,那么事務(wù) B 剛才得到的數(shù)據(jù)就是過(guò)期的數(shù)據(jù),這種現(xiàn)象就被稱(chēng)為臟讀。
不可重復(fù)讀
在一個(gè)事務(wù)內(nèi)多次讀取同一個(gè)數(shù)據(jù),如果出現(xiàn)前后兩次讀到的數(shù)據(jù)不一樣的情況,就意味著發(fā)生了「不可重復(fù)讀」現(xiàn)象。
舉個(gè)栗子。
假設(shè)有 A 和 B 這兩個(gè)事務(wù)同時(shí)在處理,事務(wù) A 先開(kāi)始從數(shù)據(jù)庫(kù)中讀取小林的余額數(shù)據(jù),然后繼續(xù)執(zhí)行代碼邏輯處理,在這過(guò)程中如果事務(wù) B 更新了這條數(shù)據(jù),并提交了事務(wù),那么當(dāng)事務(wù) A 再次讀取該數(shù)據(jù)時(shí),就會(huì)發(fā)現(xiàn)前后兩次讀到的數(shù)據(jù)是不一致的,這種現(xiàn)象就被稱(chēng)為不可重復(fù)讀。
幻讀
在一個(gè)事務(wù)內(nèi)多次查詢(xún)某個(gè)符合查詢(xún)條件的「記錄數(shù)量」,如果出現(xiàn)前后兩次查詢(xún)到的記錄數(shù)量不一樣的情況,就意味著發(fā)生了「幻讀」現(xiàn)象。
舉個(gè)栗子。
假設(shè)有 A 和 B 這兩個(gè)事務(wù)同時(shí)在處理,事務(wù) A 先開(kāi)始從數(shù)據(jù)庫(kù)查詢(xún)賬戶(hù)余額大于 100 萬(wàn)的記錄,發(fā)現(xiàn)共有 5 條,然后事務(wù) B 也按相同的搜索條件也是查詢(xún)出了 5 條記錄。
接下來(lái),事務(wù) A 插入了一條余額超過(guò) 100 萬(wàn)的賬號(hào),并提交了事務(wù),此時(shí)數(shù)據(jù)庫(kù)超過(guò) 100 萬(wàn)余額的賬號(hào)個(gè)數(shù)就變?yōu)?6。
然后事務(wù) B 再次查詢(xún)賬戶(hù)余額大于 100 萬(wàn)的記錄,此時(shí)查詢(xún)到的記錄數(shù)量有 6 條,發(fā)現(xiàn)和前一次讀到的記錄數(shù)量不一樣了,就感覺(jué)發(fā)生了幻覺(jué)一樣,這種現(xiàn)象就被稱(chēng)為幻讀。
事務(wù)的隔離級(jí)別有哪些?
前面我們提到,當(dāng)多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)可能會(huì)遇到「臟讀、不可重復(fù)讀、幻讀」的現(xiàn)象,這些現(xiàn)象會(huì)對(duì)事務(wù)的一致性產(chǎn)生不同程序的影響。
- 臟讀:讀到其他事務(wù)未提交的數(shù)據(jù);
- 不可重復(fù)讀:前后讀取的數(shù)據(jù)不一致;
- 幻讀:前后讀取的記錄數(shù)量不一致。
這三個(gè)現(xiàn)象的嚴(yán)重性排序如下:
SQL 標(biāo)準(zhǔn)提出了四種隔離級(jí)別來(lái)規(guī)避這些現(xiàn)象,隔離級(jí)別越高,性能效率就越低,這四個(gè)隔離級(jí)別如下:
- 讀未提交(read uncommitted),指一個(gè)事務(wù)還沒(méi)提交時(shí),它做的變更就能被其他事務(wù)看到;
- 讀提交(read committed),指一個(gè)事務(wù)提交之后,它做的變更才能被其他事務(wù)看到;
- 可重復(fù)讀(repeatable read),指一個(gè)事務(wù)執(zhí)行過(guò)程中看到的數(shù)據(jù),一直跟這個(gè)事務(wù)啟動(dòng)時(shí)看到的數(shù)據(jù)是一致的,MySQL InnoDB 引擎的默認(rèn)隔離級(jí)別;
- 串行化(serializable );會(huì)對(duì)記錄加上讀寫(xiě)鎖,在多個(gè)事務(wù)對(duì)這條記錄進(jìn)行讀寫(xiě)操作時(shí),如果發(fā)生了讀寫(xiě)沖突的時(shí)候,后訪(fǎng)問(wèn)的事務(wù)必須等前一個(gè)事務(wù)執(zhí)行完成,才能繼續(xù)執(zhí)行;
按隔離水平高低排序如下:
針對(duì)不同的隔離級(jí)別,并發(fā)事務(wù)時(shí)可能發(fā)生的現(xiàn)象也會(huì)不同。
也就是說(shuō):
- 在「讀未提交」隔離級(jí)別下,可能發(fā)生臟讀、不可重復(fù)讀和幻讀現(xiàn)象;
- 在「讀提交」隔離級(jí)別下,可能發(fā)生不可重復(fù)讀和幻讀現(xiàn)象,但是不可能發(fā)生臟讀現(xiàn)象;
- 在「可重復(fù)讀」隔離級(jí)別下,可能發(fā)生幻讀現(xiàn)象,但是不可能臟讀和不可重復(fù)讀現(xiàn)象;
- 在「串行化」隔離級(jí)別下,臟讀、不可重復(fù)讀和幻讀現(xiàn)象都不可能會(huì)發(fā)生。
所以,要解決臟讀現(xiàn)象,就要升級(jí)到「讀提交」以上的隔離級(jí)別;要解決不可重復(fù)讀現(xiàn)象,就要升級(jí)到「可重復(fù)讀」的隔離級(jí)別。
不過(guò),要解決幻讀現(xiàn)象不建議將隔離級(jí)別升級(jí)到「串行化」,因?yàn)檫@樣會(huì)導(dǎo)致數(shù)據(jù)庫(kù)在并發(fā)事務(wù)時(shí)性能很差。
InnoDB 引擎的默認(rèn)隔離級(jí)別雖然是「可重復(fù)讀」,但是它通過(guò)next-key lock 鎖(行鎖和間隙鎖的組合)來(lái)鎖住記錄之間的“間隙”和記錄本身,防止其他事務(wù)在這個(gè)記錄之間插入新的記錄,這樣就避免了幻讀現(xiàn)象。
接下里,舉個(gè)具體的例子來(lái)說(shuō)明這四種隔離級(jí)別,有一張賬戶(hù)余額表,里面有一條記錄:
然后有兩個(gè)并發(fā)的事務(wù),事務(wù) A 只負(fù)責(zé)查詢(xún)余額,事務(wù) B 則會(huì)將我的余額改成 200 萬(wàn),下面是按照時(shí)間順序執(zhí)行兩個(gè)事務(wù)的行為:
在不同隔離級(jí)別下,事務(wù) A 執(zhí)行過(guò)程中查詢(xún)到的余額可能會(huì)不同:
- 在「讀未提交」隔離級(jí)別下,事務(wù) B 修改余額后,雖然沒(méi)有提交事務(wù),但是此時(shí)的余額已經(jīng)可以被事務(wù) A 看見(jiàn)了,于是事務(wù) A 中余額 V1 查詢(xún)的值是 200 萬(wàn),余額 V2、V3 自然也是 200 萬(wàn)了;
- 在「讀提交」隔離級(jí)別下,事務(wù) B 修改余額后,因?yàn)闆](méi)有提交事務(wù),所以事務(wù) A 中余額 V1 的值還是 100 萬(wàn),等事務(wù) B 提交完后,最新的余額數(shù)據(jù)才能被事務(wù) A 看見(jiàn),因此額 V2、V3 都是 200 萬(wàn);
- 在「可重復(fù)讀」隔離級(jí)別下,事務(wù) A 只能看見(jiàn)啟動(dòng)事務(wù)時(shí)的數(shù)據(jù),所以余額 V1、余額 V2 的值都是 100 萬(wàn),當(dāng)事務(wù) A 提交事務(wù)后,就能看見(jiàn)最新的余額數(shù)據(jù)了,所以余額 V3 的值是 200 萬(wàn);
- 在「串行化」隔離級(jí)別下,事務(wù) B 在執(zhí)行將余額 100 萬(wàn)修改為 200 萬(wàn)時(shí),由于此前事務(wù) A 執(zhí)行了讀操作,這樣就發(fā)生了讀寫(xiě)沖突,于是就會(huì)被鎖住,直到事務(wù) A 提交后,事務(wù) B 才可以繼續(xù)執(zhí)行,所以從 A 的角度看,余額 V1、V2 的值是 100 萬(wàn),余額 V3 的值是 200萬(wàn)。
這四種隔離級(jí)別具體是如何實(shí)現(xiàn)的呢?
- 對(duì)于「讀未提交」隔離級(jí)別的事務(wù)來(lái)說(shuō),因?yàn)榭梢宰x到未提交事務(wù)修改的數(shù)據(jù),所以直接讀取最新的數(shù)據(jù)就好了;
- 對(duì)于「串行化」隔離級(jí)別的事務(wù)來(lái)說(shuō),通過(guò)加讀寫(xiě)鎖的方式來(lái)避免并行訪(fǎng)問(wèn);
- 對(duì)于「讀提交」和「可重復(fù)讀」隔離級(jí)別的事務(wù)來(lái)說(shuō),它們是通過(guò) Read View 來(lái)實(shí)現(xiàn)的,它們的區(qū)別在于創(chuàng)建 Read View 的時(shí)機(jī)不同,大家可以把 Read View 理解成一個(gè)數(shù)據(jù)快照,就像相機(jī)拍照那樣,定格某一時(shí)刻的風(fēng)景。「讀提交」隔離級(jí)別是在「每個(gè)語(yǔ)句執(zhí)行前」都會(huì)重新生成一個(gè) Read View,而「可重復(fù)讀」隔離級(jí)別是「啟動(dòng)事務(wù)時(shí)」生成一個(gè) Read View,然后整個(gè)事務(wù)期間都在用這個(gè) Read View。
注意,執(zhí)行「開(kāi)始事務(wù)」命令,并不意味著啟動(dòng)了事務(wù)。在 MySQL 有兩種開(kāi)啟事務(wù)的命令,分別是:
- 第一種:begin/start transaction 命令;
- 第二種:start transaction with consistent snapshot 命令;
這兩種開(kāi)啟事務(wù)的命令,事務(wù)的啟動(dòng)時(shí)機(jī)是不同的:
- 執(zhí)行了 begin/start transaction 命令后,并不代表事務(wù)啟動(dòng)了。只有在執(zhí)行這個(gè)命令后,執(zhí)行了增刪查改操作的 SQL 語(yǔ)句,才是事務(wù)真正啟動(dòng)的時(shí)機(jī);
- 執(zhí)行了 start transaction with consistent snapshot 命令,就會(huì)馬上啟動(dòng)事務(wù)。
接下來(lái)詳細(xì)說(shuō)下,Read View 在 MVCC 里如何工作的?
Read View 在 MVCC 里如何工作的?
我們需要了解兩個(gè)知識(shí):
- Read View 中四個(gè)字段作用;
- 聚簇索引記錄中兩個(gè)跟事務(wù)有關(guān)的隱藏列;
那 Read View 到底是個(gè)什么東西?
Read View 有四個(gè)重要的字段:
- m_ids :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫(kù)中「活躍事務(wù)」的事務(wù) id 列表,注意是一個(gè)列表,“活躍事務(wù)”指的就是,啟動(dòng)了但還沒(méi)提交的事務(wù)。
- min_trx_id :指的是在創(chuàng)建 Read View 時(shí),當(dāng)前數(shù)據(jù)庫(kù)中「活躍事務(wù)」中事務(wù) id 最小的事務(wù),也就是 m_ids 的最小值。
- max_trx_id :這個(gè)并不是 m_ids 的最大值,而是創(chuàng)建 Read View 時(shí)當(dāng)前數(shù)據(jù)庫(kù)中應(yīng)該給下一個(gè)事務(wù)的 id 值,也就是「活躍事務(wù)」中事務(wù) id 最大值 + 1;
- creator_trx_id :指的是創(chuàng)建該 Read View 的事務(wù)的事務(wù) id。
知道了 Read View 的字段,我們還需要了解聚簇索引記錄中的兩個(gè)隱藏列。
假設(shè)在賬戶(hù)余額表插入一條小林余額為 100 萬(wàn)的記錄,然后我把這兩個(gè)隱藏列也畫(huà)出來(lái),該記錄的整個(gè)示意圖如下:
對(duì)于使用 InnoDB 存儲(chǔ)引擎的數(shù)據(jù)庫(kù)表,它的聚簇索引記錄中都包含下面兩個(gè)隱藏列:
- trx_id,當(dāng)一個(gè)事務(wù)對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),就會(huì)把該事務(wù)的事務(wù) id 記錄在 trx_id 隱藏列里;
- roll_pointer,每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊版本的記錄寫(xiě)入到 undo 日志中,然后這個(gè)隱藏列是個(gè)指針,指向每一個(gè)舊版本記錄,于是就可以通過(guò)它找到修改前的記錄。
在創(chuàng)建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:
一個(gè)事務(wù)去訪(fǎng)問(wèn)記錄的時(shí)候,除了自己的更新記錄總是可見(jiàn)之外,還有這幾種情況:
- 如果記錄的 trx_id 值小于 Read View 中的 min_trx_id 值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View 前已經(jīng)提交的事務(wù)生成的,所以該版本的記錄對(duì)當(dāng)前事務(wù)可見(jiàn)。
- 如果記錄的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示這個(gè)版本的記錄是在創(chuàng)建 Read View 后才啟動(dòng)的事務(wù)生成的,所以該版本的記錄對(duì)當(dāng)前事務(wù)不可見(jiàn)。
- 如果記錄的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之間,需要判斷 trx_id 是否在 m_ids 列表中:
如果記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務(wù)依然活躍著(還沒(méi)提交事務(wù)),所以該版本的記錄對(duì)當(dāng)前事務(wù)不可見(jiàn)。
如果記錄的 trx_id 不在 m_ids 列表中,表示生成該版本記錄的活躍事務(wù)已經(jīng)被提交,所以該版本的記錄對(duì)當(dāng)前事務(wù)可見(jiàn)。
這種通過(guò)「版本鏈」來(lái)控制并發(fā)事務(wù)訪(fǎng)問(wèn)同一個(gè)記錄時(shí)的行為就叫 MVCC(多版本并發(fā)控制)。
可重復(fù)讀是如何工作的?
可重復(fù)讀隔離級(jí)別是啟動(dòng)事務(wù)時(shí)生成一個(gè) Read View,然后整個(gè)事務(wù)期間都在用這個(gè) Read View。
假設(shè)事務(wù) A (事務(wù) id 為51)啟動(dòng)后,緊接著事務(wù) B (事務(wù) id 為52)也啟動(dòng)了,那這兩個(gè)事務(wù)創(chuàng)建的 Read View 如下:
事務(wù) A 和 事務(wù) B 的 Read View 具體內(nèi)容如下:
- 在事務(wù) A 的 Read View 中,它的事務(wù) id 是 51,由于它是第一個(gè)啟動(dòng)的事務(wù),所以此時(shí)活躍事務(wù)的事務(wù) id 列表就只有 51,活躍事務(wù)的事務(wù) id 列表中最小的事務(wù) id 是事務(wù) A 本身,下一個(gè)事務(wù) id 則是 52。
- 在事務(wù) B 的 Read View 中,它的事務(wù) id 是 52,由于事務(wù) A 是活躍的,所以此時(shí)活躍事務(wù)的事務(wù) id 列表是 51 和 52,活躍的事務(wù) id 中最小的事務(wù) id 是事務(wù) A,下一個(gè)事務(wù) id 應(yīng)該是 53。
接著,在可重復(fù)讀隔離級(jí)別下,事務(wù) A 和事務(wù) B 按順序執(zhí)行了以下操作:
- 事務(wù) B 讀取小林的賬戶(hù)余額記錄,讀到余額是 100 萬(wàn);
- 事務(wù) A 將小林的賬戶(hù)余額記錄修改成 200 萬(wàn),并沒(méi)有提交事務(wù);
- 事務(wù) B 讀取小林的賬戶(hù)余額記錄,讀到余額還是 100 萬(wàn);
- 事務(wù) A 提交事務(wù);
- 事務(wù) B 讀取小林的賬戶(hù)余額記錄,讀到余額依然還是 100 萬(wàn);
接下來(lái),跟大家具體分析下。
事務(wù) B 第一次讀小林的賬戶(hù)余額記錄,在找到記錄后,它會(huì)先看這條記錄的 trx_id,此時(shí)發(fā)現(xiàn) trx_id 為 50,比事務(wù) B 的 Read View 中的 min_trx_id 值(51)還小,這意味著修改這條記錄的事務(wù)早就在事務(wù) B 啟動(dòng)前提交過(guò)了,所以該版本的記錄對(duì)事務(wù) B 可見(jiàn)的,也就是事務(wù) B 可以獲取到這條記錄。
接著,事務(wù) A 通過(guò) update 語(yǔ)句將這條記錄修改了(還未提交事務(wù)),將小林的余額改成 200 萬(wàn),這時(shí) MySQL 會(huì)記錄相應(yīng)的 undo log,并以鏈表的方式串聯(lián)起來(lái),形成版本鏈,如下圖:
你可以在上圖的「記錄的字段」看到,由于事務(wù) A 修改了該記錄,以前的記錄就變成舊版本記錄了,于是最新記錄和舊版本記錄通過(guò)鏈表的方式串起來(lái),而且最新記錄的 trx_id 是事務(wù) A 的事務(wù) id(trx_id = 51)。
然后事務(wù) B 第二次去讀取該記錄,發(fā)現(xiàn)這條記錄的 trx_id 值為 51,在事務(wù) B 的 Read View 的 min_trx_id 和 max_trx_id 之間,則需要判斷 trx_id 值是否在 m_ids 范圍內(nèi),判斷的結(jié)果是在的,那么說(shuō)明這條記錄是被還未提交的事務(wù)修改的,這時(shí)事務(wù) B 并不會(huì)讀取這個(gè)版本的記錄。而是沿著 undo log 鏈條往下找舊版本的記錄,直到找到 trx_id 「小于」事務(wù) B 的 Read View 中的 min_trx_id 值的第一條記錄,所以事務(wù) B 能讀取到的是 trx_id 為 50 的記錄,也就是小林余額是 100 萬(wàn)的這條記錄。
最后,當(dāng)事物 A 提交事務(wù)后,由于隔離級(jí)別時(shí)「可重復(fù)讀」,所以事務(wù) B 再次讀區(qū)記錄時(shí),還是基于啟動(dòng)事務(wù)時(shí)創(chuàng)建的 Read View 來(lái)判斷當(dāng)前版本的記錄是否可見(jiàn)。所以,即使事物 A 將小林余額修改為 200 萬(wàn)并提交了事務(wù), 事務(wù) B 第三次讀取記錄時(shí),讀到的記錄都是小林余額是 100 萬(wàn)的這條記錄。
就是通過(guò)這樣的方式實(shí)現(xiàn)了,「可重復(fù)讀」隔離級(jí)別下在事務(wù)期間讀到的記錄都是事務(wù)啟動(dòng)前的記錄。
讀提交是如何工作的?
讀提交隔離級(jí)別是在每次讀取數(shù)據(jù)時(shí),都會(huì)生成一個(gè)新的 Read View。
也意味著,事務(wù)期間的多次讀取同一條數(shù)據(jù),前后兩次讀的數(shù)據(jù)可能會(huì)出現(xiàn)不一致,因?yàn)榭赡苓@期間另外一個(gè)事務(wù)修改了該記錄,并提交了事務(wù)。
那讀提交隔離級(jí)別是怎么工作呢?我們還是以前面的例子來(lái)聊聊。
假設(shè)事務(wù) A (事務(wù) id 為51)啟動(dòng)后,緊接著事務(wù) B (事務(wù) id 為52)也啟動(dòng)了,接著按順序執(zhí)行了以下操作:
- 事務(wù) B 讀取數(shù)據(jù)(創(chuàng)建 Read View),小林的賬戶(hù)余額為 100 萬(wàn);
- 事務(wù) A 修改數(shù)據(jù)(還沒(méi)提交事務(wù)),將小林的賬戶(hù)余額從 100 萬(wàn)修改成了 200 萬(wàn);
- 事務(wù) B 讀取數(shù)據(jù)(創(chuàng)建 Read View),小林的賬戶(hù)余額為 100 萬(wàn);
- 事務(wù) A 提交事務(wù);
- 事務(wù) B 讀取數(shù)據(jù)(創(chuàng)建 Read View),小林的賬戶(hù)余額為 200 萬(wàn);
那具體怎么做到的呢?我們重點(diǎn)看事務(wù) B 每次讀取數(shù)據(jù)時(shí)創(chuàng)建的 Read View。前兩次 事務(wù) B 讀取數(shù)據(jù)時(shí)創(chuàng)建的 Read View 如下圖:
我們來(lái)分析下為什么事務(wù) B 第二次讀數(shù)據(jù)時(shí),讀不到事務(wù) A (還未提交事務(wù))修改的數(shù)據(jù)?
事務(wù) B 在找到小林這條記錄時(shí),會(huì)看這條記錄的 trx_id 是 51,在事務(wù) B 的 Read View 的 min_trx_id 和 max_trx_id 之間,接下來(lái)需要判斷 trx_id 值是否在 m_ids 范圍內(nèi),判斷的結(jié)果是在的,那么說(shuō)明這條記錄是被還未提交的事務(wù)修改的,這時(shí)事務(wù) B 并不會(huì)讀取這個(gè)版本的記錄。而是,沿著 undo log 鏈條往下找舊版本的記錄,直到找到 trx_id 「小于」事務(wù) B 的 Read View 中的 min_trx_id 值的第一條記錄,所以事務(wù) B 能讀取到的是 trx_id 為 50 的記錄,也就是小林余額是 100 萬(wàn)的這條記錄。
我們來(lái)分析下為什么事務(wù) A 提交后,事務(wù) B 就可以讀到事務(wù) A 修改的數(shù)據(jù)?
在事務(wù) A 提交后,由于隔離級(jí)別是「讀提交」,所以事務(wù) B 在每次讀數(shù)據(jù)的時(shí)候,會(huì)重新創(chuàng)建 Read View,此時(shí)事務(wù) B 第三次讀取數(shù)據(jù)時(shí)創(chuàng)建的 Read View 如下:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機(jī)制,建議將圖片保存下來(lái)直接上傳(img-NhC5bZpC-1648719236189)(https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/mysql/事務(wù)隔離/讀提交事務(wù)2.drawio.png)]
事務(wù) B 在找到小林這條記錄時(shí),會(huì)發(fā)現(xiàn)這條記錄的 trx_id 是 51,比事務(wù) B 的 Read View 中的 min_trx_id 值(52)還小,這意味著修改這條記錄的事務(wù)早就在創(chuàng)建 Read View 前提交過(guò)了,所以該版本的記錄對(duì)事務(wù) B 是可見(jiàn)的。
正是因?yàn)樵谧x提交隔離級(jí)別下,事務(wù)每次讀數(shù)據(jù)時(shí)都重新創(chuàng)建 Read View,那么在事務(wù)期間的多次讀取同一條數(shù)據(jù),前后兩次讀的數(shù)據(jù)可能會(huì)出現(xiàn)不一致,因?yàn)榭赡苓@期間另外一個(gè)事務(wù)修改了該記錄,并提交了事務(wù)。
總結(jié)
事務(wù)是在 MySQL 引擎層實(shí)現(xiàn)的,我們常見(jiàn)的 InnoDB 引擎是支持事務(wù)的,事務(wù)的四大特性是原子性、一致性、隔離性、持久性,我們這次主要講的是隔離性。
當(dāng)多個(gè)事務(wù)并發(fā)執(zhí)行的時(shí)候,會(huì)引發(fā)臟讀、不可重復(fù)讀、幻讀這些問(wèn)題,那為了避免這些問(wèn)題,SQL 提出了四種隔離級(jí)別,分別是讀未提交、讀已提交、可重復(fù)讀、串行化,從左往右隔離級(jí)別順序遞增,隔離級(jí)別越高,意味著性能越差,InnoDB 引擎的默認(rèn)隔離級(jí)別是可重復(fù)讀。
要解決臟讀現(xiàn)象,就要將隔離級(jí)別升級(jí)到讀已提交以上的隔離級(jí)別,要解決不可重復(fù)讀現(xiàn)象,就要將隔離級(jí)別升級(jí)到可重復(fù)讀以上的隔離級(jí)別。
而對(duì)于幻讀現(xiàn)象,不建議將隔離級(jí)別升級(jí)為串行化,因?yàn)檫@會(huì)導(dǎo)致數(shù)據(jù)庫(kù)并發(fā)時(shí)性能很差。InnoDB 引擎的默認(rèn)隔離級(jí)別雖然是「可重復(fù)讀」,但是它通過(guò) next-key lock 鎖(行鎖+間隙鎖的組合)來(lái)鎖住記錄之間的“間隙”和記錄本身,防止其他事務(wù)在這個(gè)記錄之間插入新的記錄,這樣就避免了幻讀現(xiàn)象。
- 對(duì)于「讀提交」和「可重復(fù)讀」隔離級(jí)別的事務(wù)來(lái)說(shuō),它們是通過(guò) **Read View **來(lái)實(shí)現(xiàn)的,它們的區(qū)別在于創(chuàng)建 Read View 的時(shí)機(jī)不同:
- 「讀提交」隔離級(jí)別是在每個(gè) select 都會(huì)生成一個(gè)新的 Read View,也意味著,事務(wù)期間的多次讀取同一條數(shù)據(jù),前后兩次讀的數(shù)據(jù)可能會(huì)出現(xiàn)不一致,因?yàn)榭赡苓@期間另外一個(gè)事務(wù)修改了該記錄,并提交了事務(wù)。
「可重復(fù)讀」隔離級(jí)別是啟動(dòng)事務(wù)時(shí)生成一個(gè) Read View,然后整個(gè)事務(wù)期間都在用這個(gè) Read View,這樣就保證了在事務(wù)期間讀到的數(shù)據(jù)都是事務(wù)啟動(dòng)前的記錄。
這兩個(gè)隔離級(jí)別實(shí)現(xiàn)是通過(guò)「事務(wù)的 Read View 里的字段」和「記錄中的兩個(gè)隱藏列」的比對(duì),來(lái)控制并發(fā)事務(wù)訪(fǎng)問(wèn)同一個(gè)記錄時(shí)的行為,這就叫 MVCC(多版本并發(fā)控制)。
在可重復(fù)讀隔離級(jí)別中,普通的 select 語(yǔ)句就是基于 MVCC 實(shí)現(xiàn)的快照讀,也就是不會(huì)加鎖的。而 select .. for update 語(yǔ)句就不是快照讀了,而是當(dāng)前讀了,也就是每次讀都是拿到最新版本的數(shù)據(jù),但是它會(huì)對(duì)讀到的記錄加上 next-key lock 鎖。
文章名稱(chēng):讓我們一起告別MVCC!
標(biāo)題路徑:http://m.fisionsoft.com.cn/article/dhpjpcs.html


咨詢(xún)
建站咨詢(xún)
