前言
以下的分析均在mysql的InnoDB引擎下。假設(shè)此時(shí)事務(wù)A與事務(wù)B同時(shí)執(zhí)行。
一、定義:
MVCC(Multi-Version Concurrency Control,多版本并發(fā)控制)一種并發(fā)控制機(jī)制扮授,在數(shù)據(jù)庫(kù)中用來(lái)控制并發(fā)執(zhí)行的事務(wù),控制事務(wù)隔離進(jìn)行专肪。
二刹勃、核心思想:
MVCC是通過保存數(shù)據(jù)在某個(gè)時(shí)間點(diǎn)的快照來(lái)進(jìn)行控制的。使用MVCC就是允許同一個(gè)數(shù)據(jù)記錄擁有多個(gè)不同的版本嚎尤。然后在查詢時(shí)通過添加相對(duì)應(yīng)的約束條件荔仁,就可以獲取用戶想要的對(duì)應(yīng)版本的數(shù)據(jù)。
三、基本數(shù)據(jù)結(jié)構(gòu)
1乏梁、redo log:
重做日志記錄次洼。存儲(chǔ)事務(wù)操作的最新數(shù)據(jù)記錄,方便日后使用遇骑。
2卖毁、undo log
撤回日志記錄,也稱版本鏈落萎。當(dāng)前事務(wù)未提交之前亥啦,undo log保存了當(dāng)前事務(wù)的正在操作的數(shù)據(jù)記錄的所有版本的信息,undo log中的數(shù)據(jù)可作為數(shù)據(jù)舊版本快照供其他并發(fā)事務(wù)進(jìn)行快照讀练链。每次有其它事務(wù)提交對(duì)當(dāng)前數(shù)據(jù)行的修改翔脱,都是添加到undo log中。undo log是由每個(gè)數(shù)據(jù)行的多個(gè)不同的版本鏈接在一起構(gòu)成的一個(gè)記錄“鏈表”媒鼓。如下圖:
3届吁、read_view(快照)
①read_view的簡(jiǎn)單理解:
會(huì)對(duì)數(shù)據(jù)在每個(gè)時(shí)刻的狀態(tài)拍成照片記錄下來(lái)。那么之后獲取某時(shí)刻的數(shù)據(jù)時(shí)就還是原來(lái)的照片上的數(shù)據(jù)绿鸣,是不會(huì)變的疚沐。其實(shí)也可以簡(jiǎn)單理解為是一個(gè)版本鏈的集合,只不過在這里的版本鏈?zhǔn)墙?jīng)過篩選的枚驻。
②read_view的基本結(jié)構(gòu):
read_view->creator_trx_id = 當(dāng)前事務(wù)id; # 當(dāng)前的事務(wù)id
read_view->up_limit_id = 12654; # 當(dāng)前活躍事務(wù)的最小id
read_view->low_limit_id = 12659; # 當(dāng)前活躍事務(wù)的最小id
read_view->trx_ids = [12654, 12659]; # 當(dāng)前活躍的事務(wù)的id列表濒旦,又稱活躍事務(wù)鏈表。表示在記錄當(dāng)前快照時(shí)的所有活躍的再登、未提交的事務(wù)
read_view->m_trx_ids = 2; # 當(dāng)前活躍的事務(wù)id列表長(zhǎng)度
注意:
- read_view中包含了活躍事務(wù)鏈表,這個(gè)鏈表表示此時(shí)還在活躍的事務(wù)晾剖,指的是那些在當(dāng)前快照中還未提交的事務(wù)锉矢。(注意:新建事務(wù)(當(dāng)前事務(wù))與正在內(nèi)存中commit 的事務(wù)不在活躍事務(wù)鏈表)。
- read_view中不會(huì)顯示所有的數(shù)據(jù)行齿尽,只會(huì)顯示“可見”的記錄沽损。篩選方式如下所述。
③read_view的記錄篩選方式:
前提:DATA_TRX_ID 表示每個(gè)數(shù)據(jù)行的最新的事務(wù)ID循头;up_limit_id表示當(dāng)前快照中的最先開始的事務(wù)绵估;low_limit_id表示當(dāng)前快照中的最慢開始的事務(wù),即最后一個(gè)事務(wù)卡骂。
如果記錄的DATA_TRX_ID < up_limit_id:在創(chuàng)建read_view時(shí)国裳,修改該記錄的事務(wù)已提交,該記錄可被快照中的事務(wù)讀取到(即可見)全跨。
如果DATA_TRX_ID >= low_limit_id:表示該記錄是在當(dāng)前read_view創(chuàng)建之后被其它事務(wù)修改的缝左,該記錄在當(dāng)前快照中肯定不可見。此時(shí)需要從DB_ROLL_PTR指針?biāo)赶虻幕貪L段中取出最新的undo-log的版本號(hào), 然后用它繼續(xù)重新開始整套比較算法。
-
如果up_limit_id <= DATA_TRX_ID < low_limit_i:
- 需要在活躍事務(wù)鏈表中查找是否存在ID為DATA_TRX_ID的值的事務(wù)渺杉。
- 如果存在蛇数,那么因?yàn)樵诨钴S事務(wù)鏈表中的事務(wù)是未提交的,所以該記錄是不可見的是越。此時(shí)需要從DB_ROLL_PTR指針?biāo)赶虻幕貪L段中取出最新的undo-log的版本號(hào), 然后用它繼續(xù)重新開始整套比較算法耳舅。(詳細(xì)分析為什么“不可見”:因?yàn)镈ATA_TRX_ID只有在事務(wù)提交之后才會(huì)更新,而此時(shí)因?yàn)槭聞?wù)還存在于活躍事務(wù)鏈表中倚评,所以說明事務(wù)是還沒有commit浦徊,所以此時(shí)不可能存在對(duì)應(yīng)的數(shù)據(jù)行,只有在當(dāng)前事務(wù)提交之后才會(huì)有對(duì)應(yīng)的數(shù)據(jù)行蔓纠。)
- 如果不存在辑畦,所以是可見的。(分析:按照上一點(diǎn)的對(duì)“不可見”原因的分析腿倚,可明白只能是當(dāng)前本事務(wù)更新了這條記錄纯出,因?yàn)樵诋?dāng)前read view中,只能是當(dāng)前事務(wù)和正在內(nèi)存中commit的事務(wù)不在事務(wù)活躍鏈表中敷燎,對(duì)于“正在內(nèi)存中commit的事務(wù)”暂筝,因?yàn)樗€沒有commit,所以肯定是不可能讀取到它的即將要commit的數(shù)據(jù)的硬贯,而所以只能是當(dāng)前事務(wù)對(duì)這個(gè)數(shù)據(jù)行做了修改了焕襟,雖然未提交,但是因?yàn)槭窃诋?dāng)前事務(wù)中饭豹,所以肯定是可以讀取到更新的數(shù)據(jù)的鸵赖。
④read_view的更新方式:
注意:僅分析RC級(jí)別和RR級(jí)別,因?yàn)镸VCC不適用于其它兩個(gè)隔離級(jí)別拄衰。
-
a它褪、對(duì)于Read Committed級(jí)別的:
- 基本描述:每次執(zhí)行select都會(huì)創(chuàng)建新的read_view,更新舊read_view翘悉,保證能讀取到其他事務(wù)已經(jīng)COMMIT的內(nèi)容(讀提交的語(yǔ)義)茫打;
- 詳細(xì)分析:假設(shè)當(dāng)前有事務(wù)A和事務(wù)A+1并發(fā)進(jìn)行。在當(dāng)前級(jí)別下妖混,事務(wù)A每次select的時(shí)候會(huì)創(chuàng)建新的read_view老赤,此時(shí)可以簡(jiǎn)單理解為事務(wù)A會(huì)提交,也就是讓事務(wù)A執(zhí)行完畢制市,然后創(chuàng)建一個(gè)新的事務(wù)比如是事務(wù)A+2抬旺。這樣子的話,因?yàn)槭聞?wù)A+2的事務(wù)ID肯定是比事務(wù)A+1的ID大息堂,所以就能夠讀取到事務(wù)A+1的更新了嚷狞。那么便可以讀取到在創(chuàng)建這個(gè)新的read_view之前事務(wù)A+1所提交的所有信息块促。這是RC級(jí)別下能讀取到其他事務(wù)已經(jīng)COMMIT的內(nèi)容的原因所在龄句。
-
b又兵、對(duì)于Repeatable Read級(jí)別的:
- 第一次select時(shí)更新這個(gè)read_view沥邻,以后不會(huì)再更新崇呵,后續(xù)所有的select都是復(fù)用這個(gè)read_view噩峦。所以能保證每次讀取的一致性状勤,即都是讀取第一次讀取到的內(nèi)容(可重復(fù)讀的語(yǔ)義)谴仙。
注意:通過對(duì)read view的更新方式的分析可以得出:對(duì)于InnoDB下的MVCC來(lái)說央星,RR雖然比RC隔離級(jí)別高啃洋,但是開銷反而相對(duì)少(因?yàn)椴挥妙l繁更新read_view)传货。
read_view的詳細(xì)分析:https://www.iteye.com/blog/mahl1990-2347029
四、MVCC在mysql的具體實(shí)現(xiàn):
4.1宏娄、基本數(shù)據(jù)結(jié)構(gòu)的定義:
在mysql中问裕,在實(shí)現(xiàn)MVCC時(shí),會(huì)為每一個(gè)表添加如下幾個(gè)隱藏的字段:
6字節(jié)的DATA_TRX_ID:
標(biāo)記了最新更新這條行記錄的transaction id孵坚,每處理一個(gè)事務(wù)粮宛,其值自動(dòng)設(shè)置為當(dāng)前事務(wù)ID(DATA_TRX_ID只有在事務(wù)提交之后才會(huì)更新);7字節(jié)的DATA_ROLL_PTR:
一個(gè)rollback指針卖宠,指向當(dāng)前這一行數(shù)據(jù)的上一個(gè)版本巍杈,找之前版本的數(shù)據(jù)就是通過這個(gè)指針,通過這個(gè)指針將數(shù)據(jù)的多個(gè)版本連接在一起構(gòu)成一個(gè)undo log版本鏈扛伍;6字節(jié)的DB_ROW_ID:
隱含的自增ID筷畦,如果數(shù)據(jù)表沒有主鍵,InnoDB會(huì)自動(dòng)以DB_ROW_ID產(chǎn)生一個(gè)聚簇索引刺洒。這是一個(gè)用來(lái)唯一標(biāo)識(shí)每一行的字段鳖宾;DELETE BIT位:
用于標(biāo)識(shí)當(dāng)前記錄是否被刪除,這里的不是真正的刪除數(shù)據(jù)逆航,而是標(biāo)志出來(lái)的刪除攘滩。真正意義的刪除是在commit的時(shí)候。
MVCC在二級(jí)索引結(jié)構(gòu)下的分析:https://www.cnblogs.com/stevenczp/p/8018986.html
4.2纸泡、增刪改查:
①增加:INSERT
- 設(shè)置新記錄的DATA_TRX_ID為當(dāng)前事務(wù)ID,其他的采用默認(rèn)的赖瞒。
②刪除:DELETE
- 修改DATA_TRX_ID的值為當(dāng)前的執(zhí)行刪除操作的事務(wù)的ID女揭,然后設(shè)置DELETE BIT為True,表示被刪除栏饮。
③修改:UPDATE <==> INSERT + DELETE
- 用X鎖鎖定該行(因?yàn)槭菍懖僮鳎?/li>
- 記錄redo log:將更新之后的數(shù)據(jù)記錄到redo log中吧兔,以便日后使用;
- 記錄undo log:將更新之后的數(shù)據(jù)記錄到undo log中袍嬉,設(shè)置當(dāng)前數(shù)據(jù)行的DATA_TRX_ID為當(dāng)前事務(wù)ID境蔼,回滾指針DATA_ROLL_PTR指向undo log中的當(dāng)前數(shù)據(jù)行更新之前的數(shù)據(jù)行灶平,同時(shí)設(shè)置更新之前的數(shù)據(jù)行的DATA_TRX_ID為當(dāng)前事務(wù)ID,并且設(shè)置DELETE BIT為True箍土,表示被刪除逢享。
④查找:SELECT
- 如果當(dāng)前數(shù)據(jù)行的DELETE BIT為False,只查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行(也就是數(shù)據(jù)行的DATA_TRX_ID必須小于等于當(dāng)前事務(wù)的ID)吴藻,這確保當(dāng)前事務(wù)讀取的行都是事務(wù)之前已經(jīng)存在的瞒爬,或者是由當(dāng)前事務(wù)創(chuàng)建或修改的行;
- 如果當(dāng)前數(shù)據(jù)行的DELETE BIT為True沟堡,表示被刪除侧但,那么只能返回DATA_TRX_ID的值大于當(dāng)前事務(wù)的行。獲取在當(dāng)前事務(wù)開始之前航罗,還沒有被刪除的行禀横。
注意:
- a、此時(shí)就是要去查找read_view粥血,判斷其中是否有需要的記錄柏锄;
- b、就算在當(dāng)前事務(wù)提交的時(shí)候立莉,也不會(huì)讀取到DATA_TRX_ID大于當(dāng)前事務(wù)ID的數(shù)據(jù)記錄(而默認(rèn)情況下绢彤,RR隔離級(jí)別下,當(dāng)前事務(wù)一commit蜓耻,就能夠讀取到其他事務(wù)的commit)茫舶。這也是MVCC能夠解決幻讀的原因。
五刹淌、使用MVCC核心優(yōu)勢(shì):
- 1饶氏、在mysql中,使用MVCC本質(zhì)上是為了在進(jìn)行讀操作的時(shí)候代替加鎖有勾,減少加鎖帶來(lái)的負(fù)擔(dān)疹启。
- 2、在mysql的InnoDB引擎蔼卡,并且是在RR隔離級(jí)別下喊崖,通過使用MVCC和gap鎖來(lái)解決幻讀問題。
六雇逞、MVCC與四大隔離級(jí)別的關(guān)系的分析:
分析了在MVCC的控制之下荤懂,如何實(shí)現(xiàn)四大隔離級(jí)別。
6.1塘砸、Read Uncimmitted級(jí)別:
由于存在臟讀节仿,即能讀到未提交事務(wù)的數(shù)據(jù)行,所以不適用MVCC掉蔬。原因是MVCC的DATA_TRX_ID只有在事務(wù)提交之后才會(huì)更新廊宪,而在Read uncimmitted級(jí)別下矾瘾,由于是讀取未提交的,所以說MVCC在這個(gè)級(jí)別下是不適用的箭启。
6.2壕翩、Read Committed級(jí)別:
查找操作:
分析:假設(shè)當(dāng)前有事務(wù)A、事務(wù)A+1册烈、數(shù)據(jù)B(DATA_TRX_ID為A-1)戈泼。
- 事務(wù)A進(jìn)行查找,此時(shí)找出事務(wù)ID小于它本身的赏僧,所以此時(shí)數(shù)據(jù)B可以被找到大猛;
- 如果在事務(wù)A還沒有執(zhí)行完畢的時(shí)候,事務(wù)A+1對(duì)數(shù)據(jù)B進(jìn)行了更新操作淀零,那么此時(shí)數(shù)據(jù)B的undo log則被更新為“數(shù)據(jù)B(DATA_TRX_ID為A+1)-> 數(shù)據(jù)B(DATA_TRX_ID為A-1)”挽绩;
- 此時(shí)如果事務(wù)A再次進(jìn)行查找操作,會(huì)更新read_view驾中。更新舊的read_view唉堪,并且開啟新的事務(wù)A+2。那么根據(jù)MVCC的規(guī)定肩民,就能夠找到數(shù)據(jù)B(DATA_TRX_ID為A+1)唠亚,可以找到更新之后的。這樣子的話就等價(jià)于能夠讀取到別的事務(wù)commit的最新的數(shù)據(jù)記錄持痰。這就符合RC級(jí)別的語(yǔ)義灶搜。
6.3、Repeatable Read級(jí)別:
查找操作:
分析:假設(shè)當(dāng)前有:事務(wù)A工窍、事務(wù)A+1割卖,數(shù)據(jù)B(DATA_TRX_ID為A-1)。
- 事務(wù)A進(jìn)行查找患雏,此時(shí)找出事務(wù)ID小于它本身的鹏溯,所以此時(shí)數(shù)據(jù)B可以被找到;
- 如果在事務(wù)A還沒有執(zhí)行完畢的時(shí)候淹仑,事務(wù)A+1對(duì)數(shù)據(jù)B進(jìn)行了更新操作丙挽,那么此時(shí)數(shù)據(jù)B的undo log則被更新為“數(shù)據(jù)B(DATA_TRX_ID為A+1)-> 數(shù)據(jù)B(DATA_TRX_ID為A-1)”;
- 此時(shí)如果事務(wù)A再次進(jìn)行查找操作匀借,那么根據(jù)MVCC的規(guī)定取试,還是只能找到數(shù)據(jù)B(DATA_TRX_ID為A-1)(因?yàn)锽(DATA_TRX_ID為A+1)的事務(wù)ID比當(dāng)前事務(wù)A的事務(wù)ID大,所以不會(huì)被找到)怀吻,不會(huì)找到更新之后的。這樣子的話就等價(jià)于只能夠讀取到事務(wù)A開始時(shí)讀取到的數(shù)據(jù)記錄初婆。這就符合RR級(jí)別的語(yǔ)義蓬坡。
6.4猿棉、Serialization級(jí)別:
串行化由于是會(huì)對(duì)所涉及到的表加鎖,并非行鎖屑咳,自然也就不存在行的版本控制問題
總結(jié):通過上面的分析可得:MVCC只適用于MySQL隔離級(jí)別中的讀已提交(Read committed)和可重復(fù)讀(Repeatable Read)
七萨赁、MVCC、gap鎖解決幻讀問題的分析:
前提:InnoDB引擎兆龙、RR隔離級(jí)別(gap鎖只存在于這個(gè)級(jí)別下)
7.1杖爽、首先了解數(shù)據(jù)記錄的讀取方式:快照讀和當(dāng)前讀
①快照讀:
讀快照,可以讀取數(shù)據(jù)的所有版本信息紫皇,包括舊版本的信息慰安。其實(shí)就是讀取MVCC中的read_view,同時(shí)結(jié)合MVCC進(jìn)行相對(duì)應(yīng)的控制聪铺;
select * from table where ?;
②當(dāng)前讀:
讀當(dāng)前化焕,讀取當(dāng)前數(shù)據(jù)的最新版本。而且讀取到這個(gè)數(shù)據(jù)之后會(huì)對(duì)這個(gè)數(shù)據(jù)加鎖铃剔,防止別的事務(wù)更改撒桨。
(分析:在進(jìn)行寫操作的時(shí)候就需要進(jìn)行“當(dāng)前讀”,讀取數(shù)據(jù)記錄的最新版本)
select * from table where ? lock in share mode; # 讀鎖
select * from table where ? for update; # 寫鎖
insert into table values (…);
update table set ? where ?;
delete from table where ?;
詳見:http://www.reibang.com/p/27352449bcc0
③RC和RR隔離級(jí)別下的快照讀和當(dāng)前讀:
- RC隔離級(jí)別下键兜,快照讀和當(dāng)前讀結(jié)果一樣凤类,都是讀取已提交的最新;
- RR隔離級(jí)別下普气,當(dāng)前讀結(jié)果是其他事務(wù)已經(jīng)提交的最新結(jié)果谜疤,快照讀是讀當(dāng)前事務(wù)之前讀到的結(jié)果。RR下創(chuàng)建快照讀的時(shí)機(jī)決定了讀到的版本棋电。
7.2茎截、解決幻讀問題:
①對(duì)于快照讀:通過MVCC來(lái)進(jìn)行控制的,不用加鎖赶盔。按照MVCC中規(guī)定的“語(yǔ)法”進(jìn)行增刪改查等操作企锌,以避免幻讀。(MVCC的具體內(nèi)容參見上方第1點(diǎn)到第4點(diǎn)的分析)
②對(duì)于當(dāng)前讀:通過next-key鎖(行鎖+gap鎖)來(lái)解決問題的于未。(next-key鎖的分析:mysql中的鎖)
7.3撕攒、特殊語(yǔ)句分析:
“MVCC不能根本上解決幻讀的情況?”
分析:這句話的含義是指對(duì)于快照讀烘浦,那么是可以通過MVCC來(lái)解決的抖坪;但是對(duì)于當(dāng)前讀,則必須通過next-key鎖(行鎖+gap鎖)來(lái)解決闷叉。