MVCC 筆記
MVCC為了解決什么問題址貌?
多版本并發(fā)控制吼畏,針對在并發(fā)訪問數(shù)據(jù)庫時對于數(shù)據(jù)版本的控制以及隔離性問題龙亲,Mysql使用了MVCC的思路來進(jìn)行版本控制
MVCC的MYSQL 實(shí)現(xiàn)淺析篮条?
Mysql 的 MVCC實(shí)現(xiàn)大致是通過隱藏列中的DB_ROLL_PTR字段以及undo log的方式生成數(shù)據(jù)版本鏈凉逛,在創(chuàng)建事務(wù)時生成ReadView來進(jìn)行版本比對馍驯,從而篩選出當(dāng)前事務(wù)可見的數(shù)據(jù)行
事務(wù)并發(fā)執(zhí)行會遇到的問題阁危?
臟寫(Dirty Write):一個事務(wù)修改了另一個事務(wù)未提交過的數(shù)據(jù)
臟讀(Dirty Read):一個事務(wù)讀取到了另一個事務(wù)未提交過的數(shù)據(jù)
不可重復(fù)讀(Non-Repeatable Read):一個事務(wù)只能讀取到另一個已經(jīng)提交的事務(wù)修改過的數(shù)據(jù),并且其他事務(wù)每對該數(shù)據(jù)進(jìn)行一次修改并提交后汰瘫,該事物都能查到最新的值(在一個事務(wù)中的多次查詢狂打,可以查詢到多個其他事務(wù)提交的最新值)
幻讀(Phantom):一個事務(wù)根據(jù)某些條件查詢出一些記錄之后,另一個事務(wù)又向表中插入了符合這些條件的記錄混弥,原先的事務(wù)用相同的條件再次查詢時趴乡,能把另外一個事務(wù)插入的數(shù)據(jù)也查詢出來
按問題嚴(yán)重性排序:
臟寫 > 臟讀 > 不可重復(fù)讀 > 幻讀
標(biāo)準(zhǔn)的四種 SQL事務(wù)隔離級別(并非Mysql定義)
Read UnCommittd:未提交讀
Read Committd:已提交讀
Repeatable Read:可重復(fù)讀
Serializable:可串行化
隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|
Read UnCommittd | Possible | Possible | Possible |
Read Committd | Not Possible | Possible | Possible |
Repeatable Read | Not Possible | Not Possible | Possible |
serializable | Not Possible | Not Possible | Not Possible |
也就是說:
在Read UnCommittd 隔離級別下,可能發(fā)生臟讀蝗拿、不可重復(fù)讀和幻讀問題
在Read Committd 隔離級別下晾捏,可能發(fā)生不可重復(fù)讀和幻讀問題
在Repeatable Read 隔離級別下,可能會發(fā)生幻讀(但是在Mysql中哀托,Repeatable Read隔離級別可以處理幻讀的問題)
在serializable 隔離級別下惦辛,各種問題都不會發(fā)生
至于臟寫,應(yīng)為臟寫實(shí)在太嚴(yán)重了仓手,所以無論哪個隔離級別都不允許臟寫的情況發(fā)生胖齐。
什么是版本鏈 ? undo日志 嗽冒?
undo日志:用于記錄事務(wù)中未提交的變更記錄呀伙,主要用于保證事務(wù)的原子性添坊,任何對數(shù)據(jù)的操作都會記錄到undo日志中剿另,直到提交事務(wù)或者rollback,才會進(jìn)行清理谚攒。
說起版本鏈,我們得先有行格式的概念,我們大概看一下Compact格式下所看到的一行數(shù)據(jù)的格式:
在一個正常的行信息中收津,除了記錄了用戶的真實(shí)記錄以外,innoDB還會為每條記錄都添加2個隱藏列以及一個可選列
列名 | 是否必須 | 占用空間 | 描述 |
---|---|---|---|
DB_TRX_ID | 是 | 6字節(jié) | 事務(wù)ID |
DB_ROLL_PTR | 是 | 7字節(jié) | 回滾指針 |
DB_ROW_ID | 否 | 6字節(jié) | 行id赵抢,唯一標(biāo)識一條記錄 |
DB_ROW_ID 在沒有自定義主鍵以及存在非Null的Unique鍵時才會添加該列
這里來簡單闡述一下DB_TRX_ID以及DB_ROLL_PTR在版本鏈中的作用
DB_TRX_ID:每次一個事務(wù)對某條聚簇索引記錄進(jìn)行改動時伸蚯,都會把該事務(wù)的事務(wù)id賦值給該記錄的DB_TRX_ID隱藏列憨栽,注意事務(wù)id是遞增的旁涤。
DB_ROLL_PTR:每次對某條聚簇索引記錄改動時,都會將舊的版本寫入到 undo日志中舔箭,然后然后這個隱藏列就相當(dāng)于一個指針箫章,可以通過它來找到該記錄修改前的信息烙荷。
注意insert是不會產(chǎn)生DB_ROLL_PRT的,因?yàn)閕nsert時并沒有更早的版本存在
了解到這里我們大概就能看到版本鏈的雛形了檬寂,也就是利用了DB_ROLL_PTR來鏈接上一個版本的數(shù)據(jù)终抽;
我們以一個hero表為例:
假如我們有一個hero表其中number為1的記錄name初始化為劉備,我們執(zhí)行如下兩個語句:
它的版本鏈大概就是下面這個樣子:
每次對該記錄更新后桶至,都會將舊值放到 undo 日志中昼伴,隨著更新次數(shù)的增多,所有版本都會被DB_ROLL_PRT屬性鏈接成為一個鏈表塞茅,我們把這個鏈表稱之為版本鏈亩码,版本鏈的頭節(jié)點(diǎn)就是當(dāng)前記錄最新的值季率,另外野瘦,每個版本中還包含生成該版本時對應(yīng)的事務(wù)id;
ReadView 是什么飒泻?
ReadView 可以按字面意思理解為讀視圖鞭光,也就是在事務(wù)開始時生成的一個快照,ReadView的設(shè)計(jì)主要是為了解決 "判斷版本鏈中哪個版本是當(dāng)前事務(wù)可見" 的問題
SERIALIZABLE隔離級別采用加鎖的方式來訪問記錄泞遗,而READ COMMITTED和 REPEATABLE READ隔離級別在事務(wù)的不同階段會創(chuàng)建ReadView
Read committed 隔離級別下惰许,每次讀取數(shù)據(jù)前都會生產(chǎn)一個ReadView
Repeatable Read 隔離級別下,在第一次讀取數(shù)據(jù)時生產(chǎn)一個ReadView
關(guān)于兩種隔離級別下產(chǎn)生ReadView時機(jī)不同帶來的影響史辙,后面描述
ReadView 主要組成結(jié)構(gòu):
m_ids:表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表
min_trx_id:表示在生成ReadView時當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id汹买,也就是m_ids中最小的值
max_trx_id:表示生成ReadView時系統(tǒng)中應(yīng)該分配給下一個事務(wù)的事務(wù)id值
max_trx_id并非是是m_ids中的最大值,事務(wù)id是遞增分配的聊倔,比方說現(xiàn)在有id為1晦毙,2,3這三個事務(wù)耙蔑,之后id為3的事務(wù)提交了见妒,那么一個新的讀事務(wù)在生產(chǎn)ReadView時,m_ids時就包括1和2甸陌,min_trx_id的值就是1须揣,max_trx_id的值就是4
creator_trx_id:表示生成該ReadView的事務(wù)的事務(wù)id
只有在對表中的記錄做改動時(執(zhí)行Insert、update钱豁、delete)才會為事務(wù)分配事務(wù)id耻卡,否則在一個只讀事務(wù)中,事務(wù)id都默認(rèn)為0
當(dāng)生成了這個ReadView牲尺,這樣在訪問某條記錄時卵酪,只需按照下邊的步驟判斷記錄的某個版本是否可見(可見性要求):
如果被訪問版本的 trx_id 屬性值與 ReadView 中的 creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問他自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問
如果被訪問版本的trx_id屬性值小于ReadView中的min_trx_id值凛澎,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交霹肝,所以該版本可以被當(dāng)前事務(wù)訪問
如果被訪問版本的trx_id屬性值大于ReadView中的max_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟塑煎,所以該版本不可以被當(dāng)前事務(wù)訪問
如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間沫换,那就需要判斷一下trx_id屬性是不是在m_ids列表中,如果在最铁,說明創(chuàng)建ReadView時生成該版本的事務(wù)還是活躍的讯赏,該版本不可以被訪問,如果不在冷尉,說明創(chuàng)建ReadView時生成該版本的事務(wù)已經(jīng)被提交漱挎,該版本可以被訪問
Read Committd 每次讀取數(shù)據(jù)前都生成一個ReadView
假如現(xiàn)在系統(tǒng)中有兩個事務(wù)在執(zhí)行,事務(wù)id分別是100雀哨、200:
版本鏈如下:
假設(shè)現(xiàn)在有一個使用Read Committd隔離級別的事務(wù)開始執(zhí)行:
那么這個Select1的執(zhí)行過程如下:
在執(zhí)行SELECT 語句時會現(xiàn)生成一個ReadView磕谅,ReadView的m_ids列表內(nèi)容為[100,200],min_trx_id為100,max_trx_id為201雾棺,creator_trx_id為0
然后從版本鏈中挑選可見記錄膊夹,從圖中可以看出,最新版本的列name的內(nèi)容是 '張飛'捌浩,該版本的事務(wù)id為100放刨,在m_ids內(nèi),所以不符合可見性要求尸饺,根據(jù)DB_ROLL_PTR(roll_pointer)找到下一個版本
下一個版本的列name的值為 '關(guān)羽'进统,該數(shù)據(jù)的事務(wù)id也為100,在m_ids范圍內(nèi)浪听,不符合可見性要求螟碎,繼續(xù)跳到下一個版本
下一個版本的列name的值為 '劉備',該版本的事務(wù)id為80馋辈,小于ReadView中的min_trx_id值100抚芦,所以這個版本符合可見性要求,最終返回給用戶的就是這條name列為 '劉備' 的數(shù)據(jù)
之后迈螟,我們把事務(wù)id為100的事務(wù)提交一下:
然后再到事務(wù)id為200的事務(wù)中更新一下hero表中number為1的數(shù)據(jù):
此刻表hero中number為1的記錄的版本鏈如下:
然后我們再到剛才使用Read Committd隔離級別的事務(wù)中繼續(xù)查找這個number為1的記錄叉抡,如下:
其中的Select2的執(zhí)行過程如下:
在執(zhí)行SELECT2 語句時又會單獨(dú)生成一個ReadView,該ReadView的m_ids列表內(nèi)容是[200](事務(wù)id為100的那個事務(wù)已經(jīng)提交了答毫,所以再次生成快照時就沒有它了)褥民,min_trx_id為200,max_trx_id為201洗搂,creator_id為0
然后從版本鏈中挑選可見的記錄消返,從圖中可以看出载弄,最新版本的列name值為 '諸葛亮',該版本的事務(wù)id為200撵颊,在m_ids列表內(nèi)宇攻,所以不符合可見性要求,根據(jù)DB_ROLL_PTR(roll_pointer)跳到下一個版本倡勇。
下一個版本的列name的值為 '趙云'逞刷,該版本的事務(wù)id為200,在m_ids列表內(nèi)妻熊,所以也不符合要求夸浅,繼續(xù)跳到下一個版本
下一個版本的列name的值為 '張飛',該版本的事務(wù)id為100扔役,小于ReadView中min_trx_id的值200帆喇,所以符合要求,最后返回給用戶的版本就是這條列name為 '張飛' 的記錄
可以看到在Read Committd的隔離級別下亿胸,出現(xiàn)了不可重復(fù)讀的場景
在Read Committd隔離級別下坯钦,事務(wù)在每次查詢開始時都會創(chuàng)建一個獨(dú)立的ReadView,關(guān)于Repeatable Read隔離級別下版本鏈以及執(zhí)行過程大概類似這里就不闡述了(歡迎討論)损敷,只是在Repeatable Read隔離級別下葫笼,在事務(wù)中多次讀數(shù)據(jù)時深啤,只會在第一次讀取數(shù)據(jù)時創(chuàng)建ReadView拗馒,后面的查詢都會復(fù)用第一次創(chuàng)建的ReadView,這就保證了前后兩次查詢到的結(jié)果一致溯街,可以嘗試使用Repeatable Read隔離級別的特性去看看上面的版本鏈诱桂,select2在Repeatable Read級別下應(yīng)該返回什么?怎么去理解可重復(fù)度呈昔?
總結(jié):
從上邊的描述中我們可以看出來挥等,所謂的MVCC(Multi-Version Concurrency Control ,多版本并發(fā)控制)指的就是在使用Read Committd堤尾、Repeatable Read這兩種隔離級別的事務(wù)在執(zhí)行普通的SEELCT操作時訪問記錄的版本鏈的過程肝劲,這樣子可以使不同事務(wù)的讀-寫、寫-讀操作并發(fā)執(zhí)行郭宝,從而提升系統(tǒng)性能辞槐。Read Committd、Repeatable Read這兩個隔離級別的一個很大不同就是:生成ReadView的時機(jī)不同粘室,Read Committd在每一次進(jìn)行普通SELECT操作前都會生成一個ReadView榄檬,而Repeatable Read只在第一次進(jìn)行普通SELECT操作前生成一個ReadView,之后的查詢操作都重復(fù)使用這個ReadView就好了衔统。
疑問鹿榜?
undo log理論上會在事務(wù)提交后進(jìn)行刪除海雪,那么版本鏈如何形成呢?
實(shí)際 insert undo 在事務(wù)提交之后就可以被釋放了舱殿,update undo由于還需要支持MVCC奥裸,不能立即刪除掉,實(shí)際在行結(jié)構(gòu)中除了隱藏列還有一個delete mark的標(biāo)記位沪袭,1代表刪除刺彩,0代表未刪除,用來記錄數(shù)據(jù)是否被刪除枝恋,所以在上面的版本鏈判斷數(shù)據(jù)時并非是簡單的判斷事務(wù)id创倔,同時還會考慮這個delete_mark標(biāo)記,同時在mysql中焚碌,作者為了減少因?yàn)橐瞥龜?shù)據(jù)后的磁盤重新排列的性能問題畦攘,還搞了一個所謂的垃圾鏈表,在這個鏈表中的記錄占用的空間稱之為所謂的可重用空間十电,之后如果有新記錄要插入到表中的話知押,可能會把這些被刪除記錄占用的存儲空間給覆蓋掉,當(dāng)然也并非所有被標(biāo)記了刪除都數(shù)據(jù)都是覆蓋處理鹃骂,這里就涉及到mysql的后臺的purge線程的作用了台盯,后面再去了解