MVCC
mvcc
指的是一種提高并發(fā)的技術(shù)檩赢。最早的數(shù)據(jù)庫系統(tǒng),只有讀讀之間可以并發(fā)违寞,讀寫贞瞒,寫讀,寫寫都要阻塞趁曼。引入多版本之后军浆,只有寫寫之間相互阻塞,其他三種操作都可以并行挡闰,這樣大幅度提高了InnoDB的并發(fā)度乒融。在內(nèi)部實現(xiàn)中,與Postgres在數(shù)據(jù)行上實現(xiàn)多版本不同尿这,InnoDB是在undolog中實現(xiàn)的簇抵,通過undolog可以找回數(shù)據(jù)的歷史版本。找回的數(shù)據(jù)歷史版本可以提供給用戶讀(按照隔離級別的定義射众,有些讀請求只能看到比較老的數(shù)據(jù)版本)碟摆,也可以在回滾的時候覆蓋數(shù)據(jù)頁上的數(shù)據(jù)。在InnoDB內(nèi)部中叨橱,會記錄一個全局的活躍讀寫事務(wù)數(shù)組典蜕,其主要用來判斷事務(wù)的可見性
MySQL的大多數(shù)事務(wù)型存儲引擎實現(xiàn)的其實都不是簡單的行級鎖。基于提升并發(fā)性能的考慮, 它們一般都同時實現(xiàn)了多版本并發(fā)控制(MVCC)罗洗。不僅是MySQL, 包括Oracle,PostgreSQL等其他數(shù)據(jù)庫系統(tǒng)也都實現(xiàn)了MVCC, 但各自的實現(xiàn)機(jī)制不盡相同, 因為MVCC沒有一個統(tǒng)一的實現(xiàn)標(biāo)準(zhǔn)愉舔。
可以認(rèn)為MVCC是行級鎖的一個變種, 但是它在很多情況下避免了加鎖操作, 因此開銷更低。雖然實現(xiàn)機(jī)制有所不同, 但大都實現(xiàn)了非阻塞的讀操作伙菜,寫操作也只鎖定必要的行轩缤。
MVCC的實現(xiàn)方式有多種, 典型的有樂觀(optimistic)并發(fā)控制 和 悲觀(pessimistic)并發(fā)控制。
MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作贩绕。其他兩個隔離級別夠和MVCC不兼容, 因為 READ UNCOMMITTED 總是讀取最新的數(shù)據(jù)行, 而不是符合當(dāng)前事務(wù)版本的數(shù)據(jù)行火的。而 SERIALIZABLE 則會對所有讀取的行都加鎖。
MVCC的實現(xiàn)依賴于:三個隱藏字段淑倾、Undo log和Read View
隱藏列
MySQL中會為每一行記錄生成隱藏列馏鹤,接下來就讓我們了解一下這幾個隱藏列吧。
(1)DB_TRX_ID:事務(wù)ID娇哆,是根據(jù)事務(wù)產(chǎn)生時間順序自動遞增的湃累,是獨一無二的勃救。如果某個事務(wù)執(zhí)行過程中對該記錄執(zhí)行了增、刪治力、改操作蒙秒,那么InnoDB存儲引擎就會記錄下該條事務(wù)的id。
(2)DB_ROLL_PTR:回滾指針宵统,本質(zhì)上就是一個指向記錄對應(yīng)的undo log的一個指針税肪,大小為 7 個字節(jié),InnoDB 便是通過這個指針找到之前版本的數(shù)據(jù)榜田。該行記錄上所有舊版本,在undo log中都通過鏈表的形式組織锻梳。
(3)DB_ROW_ID:行標(biāo)識(隱藏單調(diào)自增 ID)箭券,如果表沒有主鍵,InnoDB 會自動生成一個隱藏主鍵疑枯,大小為 6 字節(jié)辩块。如果數(shù)據(jù)表沒有設(shè)置主鍵,會以它產(chǎn)生聚簇索引荆永。
undo log
每當(dāng)我們要對一條記錄做改動時(這里的改動可以指INSERT废亭、DELETE、UPDATE)具钥,都需要把回滾時所需的東西記錄下來, 比如:
Insert undo log :插入一條記錄時豆村,至少要把這條記錄的主鍵值記下來,之后回滾的時候只需要把這個主鍵值對應(yīng)的記錄刪掉就好了骂删。
Delete undo log:刪除一條記錄時掌动,至少要把這條記錄中的內(nèi)容都記下來,這樣之后回滾時再把由這些內(nèi)容組成的記錄插入到表中就好了宁玫。
Update undo log:修改一條記錄時粗恢,至少要把修改這條記錄前的舊值都記錄下來,這樣之后回滾時再把這條記錄更新為舊值就好了欧瘪。InnoDB把這些為了回滾而記錄的這些東西稱之為undo log眷射。這里需要注意的一點是,由于查詢操作(SELECT)并不會修改任何用戶記錄佛掖,所以在查詢操作執(zhí)行時妖碉,并不需要記錄相應(yīng)的undo log。
每次對記錄進(jìn)行改動都會記錄一條undo日志苦囱,每條undo日志也都有一個DB_ROLL_PTR屬性嗅绸,可以將這些undo日志都連起來,串成一個鏈表撕彤,形成版本鏈鱼鸠。版本鏈的頭節(jié)點就是當(dāng)前記錄最新的值猛拴。
Read View
在可重復(fù)讀隔離級別下,我們可以把每一次普通的select查詢(不加for update語句)當(dāng)作一次快照讀蚀狰,而快照便是進(jìn)行select的那一刻愉昆,生成的當(dāng)前數(shù)據(jù)庫系統(tǒng)中所有未提交的事務(wù)id數(shù)組(數(shù)組里最小的id為min_id)和已經(jīng)創(chuàng)建的最大事務(wù)id(max_id)的集合,即我們所說的一致性視圖readview麻蹋。在進(jìn)行快照讀的過程中要根據(jù)一定的規(guī)則將版本鏈中每個版本的事務(wù)id與readview進(jìn)行匹配查詢我們需要的結(jié)果跛溉。
快照讀是不會看到別的事務(wù)插入的數(shù)據(jù)的。因此扮授,幻讀在“當(dāng)前讀”下才會出現(xiàn)芳室。快照讀的實現(xiàn)是基于多版本并發(fā)控制刹勃,即MVCC堪侯,可以認(rèn)為MVCC是行鎖的一個變種,但它在很多情況下荔仁,避免了加鎖操作伍宦,降低了開銷;既然是基于多版本乏梁,即快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本次洼,而有可能是之前的歷史版本。MVCC只在 READ COMMITTED 和 REPEATABLE READ兩個隔離級別下工作遇骑,其他兩個隔離級別不和MVCC不兼容卖毁。因為READ UNCOMMITTED總是讀取最新的數(shù)據(jù)行,而不是符合當(dāng)前事務(wù)版本的數(shù)據(jù)行质蕉,而SERIALIZABLE 則會對所有讀取的行都加鎖势篡。事務(wù)的快照時間點(即下文中說到的Read View的生成時間)是以第一個select來確認(rèn)的。所以即便事務(wù)先開始模暗,但是select在后面的事務(wù)的update之類的語句后進(jìn)行禁悠,那么它是可以獲取前面的事務(wù)的對應(yīng)的數(shù)據(jù)。
對于使用RC和RR隔離級別的事務(wù)來說兑宇,都必須保證讀到已經(jīng)提交了的事務(wù)修改過的記錄碍侦,也就是說假如另一個事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的隶糕。核心問題就是:需要判斷一下版本鏈中的哪個版本是當(dāng)前事務(wù)可見的瓷产。為此,InnoDB提出了一個Read View的概念枚驻。
Read View就是事務(wù)進(jìn)行快照讀(普通select查詢)操作的時候生產(chǎn)的一致性讀視圖濒旦,在該事務(wù)執(zhí)行的快照讀的那一刻,會生成數(shù)據(jù)庫系統(tǒng)當(dāng)前的一個快照再登,它由執(zhí)行查詢時所有未提交的事務(wù)id數(shù)組(數(shù)組里最小的id為min_id)和已經(jīng)創(chuàng)建的最大事務(wù)id(max_id)組成尔邓,查詢的數(shù)據(jù)結(jié)果需要跟read view做對比從而得到快照結(jié)果晾剖。
快照規(guī)則
版本鏈比對規(guī)則:
如果落在綠色部分(trx_id<min_id),表示這個版本是已經(jīng)提交的事務(wù)生成的梯嗽,這個數(shù)據(jù)是可見的齿尽;
如果落在紅色部分(trx_id>max_id),表示這個版本是由將來啟動的事務(wù)生成的灯节,是肯定不可見的循头;
如果落在黃色部分(min_id<=trx_id<=max_id),那就包含兩種情況:
a.若row的trx_id在數(shù)組中炎疆,表示這個版本是由還沒提交的事務(wù)生成的卡骂,不可見;
b.若row的trx_id不在數(shù)組中形入,表示這個版本是已經(jīng)提交了的事務(wù)生成的偿警,可見。
演示:
當(dāng)我們執(zhí)行到第一個select的語句時唯笙,會生成readview[100,200],300,
[100,200] : 未提交事務(wù)數(shù)組
300:已創(chuàng)建的最大事務(wù)id
分析:
1.第一個查詢,當(dāng)前事務(wù)id:300 ,落在黃色區(qū)域盒使,是可見的崩掘,所以結(jié)果:馬云01
2.第二個查詢,當(dāng)前事務(wù)id:100 ,落在黃色區(qū)域少办,數(shù)據(jù)在未提交的數(shù)組中苞慢,所以不可見
就需要往前一個版本找,前一個版本事務(wù)Id:100,落在黃色區(qū)域英妓,所以也是不可見的挽放,
接著往前找,數(shù)據(jù)不在未提交的數(shù)組中蔓纠,所以可見辑畦,? 結(jié)果:馬云01
3.第三個查詢,因為是InnoDB的RR模式腿倚,所以readview不會更改纯出,仍為readview[100,200],300 當(dāng)前事務(wù)id:200,落在黃色區(qū)域,數(shù)據(jù)在未提交數(shù)組中敷燎,所以不可見暂筝,需要往前一個版本找,同第二個查詢硬贯,一直到找到事務(wù)id:300? ? 結(jié)果:馬云01
再來看一種情況:
如果重新開啟一個查詢
這個時候 事務(wù)100 300 都提交了焕襟,所以活躍事務(wù)數(shù)組中只有【200】? readView [200] 300
查詢時:事務(wù)id :200? 落在黃色區(qū)域,在數(shù)組中饭豹,不可見鸵赖,往前找 找到100 時务漩,在綠色區(qū)域 可見
所以結(jié)果? :馬云03
InnoDB下的RC模式,上文中提到的RC模式的數(shù)據(jù)讀都是讀最新的即當(dāng)前讀卫漫,所以readview是實時生成的
當(dāng)執(zhí)行到查詢的時候菲饼,readView[200] 300? 所以結(jié)果同樣為 馬云03
show global variables like '%isolation%';
set global transaction_isolation ='read-committed';
set global transaction_isolation ='repeatable-read';
Uodo log 什么時候刪除:
系統(tǒng)會判斷,沒有比這個undo log更早的read view的時候列赎,undo log會被刪除宏悦。所以這里也就是為什么我們建議你盡量不要使用長事務(wù)的原因。長事務(wù)意味著系統(tǒng)里面會存在很老的事務(wù)視圖包吝。由于這些事務(wù)隨時可能訪問數(shù)據(jù)庫里面的任何數(shù)據(jù)饼煞,所以這個事務(wù)提交之前,數(shù)據(jù)庫里面它可能用到的回滾記錄都必須保留诗越,這就會導(dǎo)致大量占用存儲空間