0 - 前言
周末在家值班,看了一下MySQL的MVCC實(shí)現(xiàn)方式纲辽。之前我認(rèn)為的MVCC:
- 每行數(shù)據(jù)都存在一個(gè)版本颜武,每次數(shù)據(jù)更新時(shí)都更新該版本;
- 修改時(shí)Copy出當(dāng)前版本隨意修改拖吼,各個(gè)事務(wù)之間無(wú)干擾鳞上;
- 保存時(shí)比較版本號(hào),如果成功(commit)吊档,則覆蓋原記錄篙议;失敗則放棄copy(rollback);
就是每行都有版本號(hào)怠硼,保存時(shí)根據(jù)版本號(hào)決定是否成功涡上,有點(diǎn)樂(lè)觀(guān)鎖的意思趾断。
結(jié)果,我還是太年輕了……吩愧,Innodb的實(shí)現(xiàn)方式是:
- 事務(wù)以排他鎖的形式修改原始數(shù)據(jù);
- 把修改前的數(shù)據(jù)存放于undo log增显,通過(guò)回滾指針與主數(shù)據(jù)關(guān)聯(lián)雁佳;
- 修改成功(commit),嘛都不做同云,失敗則恢復(fù)undo log中的數(shù)據(jù)(rollback)糖权;
二者最本質(zhì)的區(qū)別是,當(dāng)修改數(shù)據(jù)時(shí)是否要排他鎖定炸站,如果鎖定了還算不算是MVCC星澳?
個(gè)人感覺(jué),Innodb的實(shí)現(xiàn)真算不上MVCC旱易,因?yàn)椴](méi)有實(shí)現(xiàn)核心的多版本共存禁偎,undo log中的內(nèi)容只是串行化的結(jié)果,記錄了多個(gè)事務(wù)的過(guò)程阀坏,不屬于多版本共存如暖。但理想的MVCC是難以實(shí)現(xiàn)的,當(dāng)事務(wù)僅修改一行記錄使用理想的MVCC模式是沒(méi)有問(wèn)題的忌堂,可以通過(guò)比較版本號(hào)進(jìn)行回滾盒至;但當(dāng)事務(wù)影響到多行數(shù)據(jù)時(shí),理想的MVCC據(jù)無(wú)能為力了士修。
比如枷遂,如果Transaciton1執(zhí)行理想的MVCC,修改Row1成功棋嘲,而修改Row2失敗酒唉,此時(shí)需要回滾Row1,但因?yàn)镽ow1沒(méi)有被鎖定封字,其數(shù)據(jù)可能又被Transaction2所修改黔州,如果此時(shí)回滾Row1的內(nèi)容,則會(huì)破壞Transaction2的修改結(jié)果阔籽,導(dǎo)致Transaction2違反ACID流妻。
理想MVCC難以實(shí)現(xiàn)的根本原因在于企圖通過(guò)樂(lè)觀(guān)鎖代替二階段提交。修改兩行數(shù)據(jù)笆制,但為了保證其一致性绅这,與修改兩個(gè)分布式系統(tǒng)中的數(shù)據(jù)并無(wú)區(qū)別,而二階段提交是目前這種場(chǎng)景保證一致性的唯一手段在辆。二階段提交的本質(zhì)是鎖定证薇,樂(lè)觀(guān)鎖的本質(zhì)是消除鎖定度苔,二者矛盾,故理想的MVCC難以真正在實(shí)際中被應(yīng)用浑度,Innodb只是借了MVCC這個(gè)名字寇窑,提供了讀的非阻塞而已。
下面看看MySQL的MVCC是怎么實(shí)現(xiàn)的箩张。
1 - Innodb的事務(wù)
MySQL的MVCC這個(gè)說(shuō)法其實(shí)不準(zhǔn)確甩骏,準(zhǔn)確來(lái)說(shuō),應(yīng)該是MySQL的Innodb引擎是如何實(shí)現(xiàn)MVCC的
Innodb為每行記錄都實(shí)現(xiàn)了三個(gè)隱藏字段:
- 6字節(jié)的事務(wù)ID先慷;
- 7字節(jié)的回滾指針饮笛;
- 隱藏的行號(hào);
為了支持事務(wù)论熙,Innodb引入了下面幾個(gè)概念:
- redo log
redo log就是保存執(zhí)行的SQL語(yǔ)句到一個(gè)指定的Log文件福青,當(dāng)Mysql執(zhí)行recovery時(shí)重新執(zhí)行redo log記錄的SQL操作即可。當(dāng)客戶(hù)端執(zhí)行每條SQL(更新語(yǔ)句)時(shí)脓诡,redo log會(huì)被首先寫(xiě)入log buffer无午;當(dāng)客戶(hù)端執(zhí)行COMMIT命令時(shí),log buffer中的內(nèi)容會(huì)被視情況刷新到磁盤(pán)誉券。redo log在磁盤(pán)上作為一個(gè)獨(dú)立的文件存在指厌,即Innodb的log文件。 - undo log
與redo log相反踊跟,undo log是為回滾而用踩验,具體內(nèi)容就是copy事務(wù)前的數(shù)據(jù)庫(kù)內(nèi)容(行)到undo buffer,在適合的時(shí)間把undo buffer中的內(nèi)容刷新到磁盤(pán)商玫。undo buffer與redo buffer一樣箕憾,也是環(huán)形緩沖,但當(dāng)緩沖滿(mǎn)的時(shí)候拳昌,undo buffer中的內(nèi)容會(huì)也會(huì)被刷新到磁盤(pán)袭异;與redo log不同的是,磁盤(pán)上不存在單獨(dú)的undo log文件炬藤,所有的undo log均存放在主ibd數(shù)據(jù)文件中(表空間)御铃,即使設(shè)置了每表一個(gè)數(shù)據(jù)文件,仍然會(huì)存在每個(gè)表的ibd中沈矿。 - rollback segment
undo log被劃分為多個(gè)段上真,具體某行的undo log就保存在某個(gè)段中,稱(chēng)為回滾段羹膳∷ィ可以認(rèn)為undo log和回滾段是同一意思。 - 鎖
Innodb提供了基于行的鎖,如果行的數(shù)量非常大就珠,則在高并發(fā)下鎖的數(shù)量也可能會(huì)比較大寇壳,據(jù)Innodb文檔說(shuō),Innodb對(duì)鎖進(jìn)行了空間有效優(yōu)化妻怎,即使并發(fā)量高也不會(huì)導(dǎo)致內(nèi)存耗盡壳炎。
對(duì)行的鎖有分兩種:排他鎖、共享鎖逼侦。共享鎖針對(duì)讀冕广,排他鎖針對(duì)寫(xiě),完全等同讀寫(xiě)鎖的概念偿洁。如果某個(gè)事務(wù)在更新某行(排他鎖),則其他事物無(wú)論是讀還是寫(xiě)本行都必須等待沟优;如果某個(gè)事物讀某行(共享鎖)涕滋,則其他讀的事物無(wú)需等待,而寫(xiě)事物則需等待挠阁。通過(guò)共享鎖宾肺,保證了多讀之間的無(wú)等待性,但是鎖的應(yīng)用又依賴(lài)Mysql的事務(wù)隔離級(jí)別侵俗。 - 隔離級(jí)別
隔離級(jí)別用來(lái)限制事務(wù)直接的交互程度锨用,目前有幾個(gè)工業(yè)標(biāo)準(zhǔn):- READ_UNCOMMITTED:讀為提交
- READ_COMMITTED:讀提交
- REPEATABLE_READ:重復(fù)讀
- SERIALIZABLE:串行化
Innodb對(duì)四種類(lèi)型都支持,臟讀和串行化應(yīng)用場(chǎng)景不多隘谣,讀提交增拥、重復(fù)讀用的比較廣泛。
2 - Innodb更新行記錄
寫(xiě)入新記錄
F1~F6是某行列的名字寻歧,1~6是其對(duì)應(yīng)的數(shù)據(jù)掌栅。后面三個(gè)隱含字段分別對(duì)應(yīng)該行的事務(wù)號(hào)和回滾指針,假如這條數(shù)據(jù)是剛INSERT的码泛,可以認(rèn)為ID為1猾封,其他兩個(gè)字段為空。
事務(wù)1更改該行的各字段的值
當(dāng)事務(wù)1更改該行的值時(shí)噪珊,會(huì)進(jìn)行如下操作:
- 用排他鎖鎖定該行
- 記錄redo log
- 把該行修改前的值Copy到undo log晌缘,即上圖中下面的行
- 修改當(dāng)前行的值,填寫(xiě)事務(wù)編號(hào)痢站,使回滾指針指向undo log中的修改前的行
事務(wù)2修改該行的值
與事務(wù)1相同磷箕,此時(shí)undo log,中有有兩行記錄瑟押,并且通過(guò)回滾指針連在一起搀捷。
因此,如果undo log一直不刪除,則會(huì)通過(guò)當(dāng)前記錄的回滾指針回溯到該行創(chuàng)建時(shí)的初始內(nèi)容嫩舟,所幸的時(shí)在Innodb中存在purge線(xiàn)程氢烘,它會(huì)查詢(xún)那些比現(xiàn)在最老的活動(dòng)事務(wù)還早的undo log,并刪除它們家厌,從而保證undo log文件不至于無(wú)限增長(zhǎng)播玖。
當(dāng)事務(wù)正常提交時(shí)Innbod只需要更改事務(wù)狀態(tài)為COMMIT即可,不需做其他額外的工作饭于,而Rollback則稍微復(fù)雜點(diǎn)蜀踏,需要根據(jù)當(dāng)前回滾指針從undo log中找出事務(wù)修改前的版本,并恢復(fù)掰吕。如果事務(wù)影響的行非常多果覆,回滾則可能會(huì)變的效率不高,根據(jù)經(jīng)驗(yàn)值沒(méi)事務(wù)行數(shù)在1000~10000之間殖熟,Innodb效率還是非常高的局待。很顯然,Innodb是一個(gè)COMMIT效率比Rollback高的存儲(chǔ)引擎菱属。
3 - Read View
上面說(shuō)到行記錄通過(guò)回滾指針串在一起父虑,形成了一個(gè)鏈绣夺,這里叫他版本鏈束世。已提交讀和可重復(fù)讀的區(qū)別就在于它們生成ReadView的策略不同输钩。
ReadView中主要就是有個(gè)列表來(lái)存儲(chǔ)系統(tǒng)中當(dāng)前活躍著的讀寫(xiě)事務(wù),也就是begin了還未提交的事務(wù)赏陵。通過(guò)這個(gè)列表來(lái)判斷記錄的某個(gè)版本是否對(duì)當(dāng)前事務(wù)可見(jiàn)饼齿。假設(shè)當(dāng)前列表里的事務(wù)id為[80,100]。
- 如果要訪(fǎng)問(wèn)的記錄版本的事務(wù)id為50瘟滨,比當(dāng)前列表最小的id 80還小候醒,那說(shuō)明這個(gè)事務(wù)在之前就提交了,所以對(duì)當(dāng)前活動(dòng)的事務(wù)來(lái)說(shuō)是可訪(fǎng)問(wèn)的杂瘸。
- 如果要訪(fǎng)問(wèn)的記錄版本的事務(wù)id為90倒淫,發(fā)現(xiàn)此事務(wù)在列表id最大值和最小值之間,那就再判斷一下是否在列表內(nèi)败玉,如果在那就說(shuō)明此事務(wù)還未提交敌土,所以版本不能被訪(fǎng)問(wèn)。如果不在那說(shuō)明事務(wù)已經(jīng)提交运翼,所以版本可以被訪(fǎng)問(wèn)返干。
- 如果要訪(fǎng)問(wèn)的記錄版本的事務(wù)id為110,那比事務(wù)列表最大id 100都大血淌,那說(shuō)明這個(gè)版本是在ReadView生成之后才發(fā)生的矩欠,所以不能被訪(fǎng)問(wèn)财剖。
這些記錄都是去版本鏈里面找的,先找最近記錄癌淮,如果最近這一條記錄事務(wù)id不符合條件躺坟,不可見(jiàn)的話(huà),再去找上一個(gè)版本再比較當(dāng)前事務(wù)的id和這個(gè)版本事務(wù)id看能不能訪(fǎng)問(wèn)乳蓄,以此類(lèi)推直到返回可見(jiàn)的版本或者結(jié)束咪橙。
也就是說(shuō)已提交讀隔離級(jí)別下的事務(wù)在每次查詢(xún)的開(kāi)始都會(huì)生成一個(gè)獨(dú)立的ReadView,而可重復(fù)讀隔離級(jí)別則在第一次讀的時(shí)候生成一個(gè)ReadView虚倒,之后的讀都復(fù)用之前的ReadView美侦。
4 - 結(jié)尾
這么看,InnoDB的雖然不是真正的多版本魂奥,但也不是說(shuō)就無(wú)處可用菠剩,對(duì)一些一致性要求不高的場(chǎng)景和對(duì)單一數(shù)據(jù)的操作的場(chǎng)景還是可以發(fā)揮作用的,比如多個(gè)事務(wù)同時(shí)更改用戶(hù)在線(xiàn)數(shù)耻煤,如果某個(gè)事務(wù)更新失敗則重新計(jì)算后重試赠叼,直至成功。這樣使用MVCC會(huì)極大地提高并發(fā)數(shù)违霞,并消除線(xiàn)程鎖。