MVCC出現(xiàn)背景
事務(wù)的4個(gè)隔離級(jí)別以及對(duì)應(yīng)的三種異常
讀未提交(Read uncommitted)
讀已提交(Read committed):臟讀
可重復(fù)讀(Repeatable read):不可重復(fù)讀
串行化(Serializable):幻讀
- 臟讀:一個(gè)事務(wù)讀取到了另外一個(gè)事務(wù)沒有提交的數(shù)據(jù);
- 不可重復(fù)讀:在同一個(gè)事務(wù)中,兩次讀取同一數(shù)據(jù),得到內(nèi)容不同;
- 幻讀:同一事務(wù)中,用同樣的操作讀取兩次,得到的記錄數(shù)不同。
在MySQL中,默認(rèn)的隔離級(jí)別是可重復(fù)讀,可以解決臟讀和不可重復(fù)讀的問題,但不能解決幻讀的問題。如果我們需要解決幻讀的問題,就需要采用串行化的方式,也就是將隔離級(jí)別提升到最高,但這樣依賴就會(huì)大幅降低數(shù)據(jù)庫的事務(wù)并發(fā)能力。
而MVCC就是通過樂觀鎖的方式來解決不可重復(fù)讀和幻讀的問題,它可以在大多數(shù)情況下替代行級(jí)鎖,降低系統(tǒng)的開銷。
MySQL并發(fā)事務(wù)會(huì)引起更新丟失問題,解決辦法是鎖,主要分兩類:
- 樂觀鎖:
? 其實(shí)就如它的名字一樣,非常樂觀,總是假設(shè)比較好的情況。
? 每次取數(shù)據(jù)的時(shí)候都認(rèn)為他人不會(huì)對(duì)其修改,所以不會(huì)上鎖,但是會(huì)在更新的時(shí)候進(jìn)行判斷,看看在此期間內(nèi)有沒有人去更新這個(gè)數(shù) 據(jù),可以使用版本號(hào)機(jī)制和CAS算法實(shí)現(xiàn)。
- 悲觀鎖:
? 與樂觀鎖完全相反,非常悲觀,總是假設(shè)非常糟糕的情況。
? 每次取數(shù)據(jù)的時(shí)候都認(rèn)為他人會(huì)對(duì)其進(jìn)行修改,所以每次拿數(shù)據(jù)的時(shí)候都會(huì)加上鎖,這樣別人想拿數(shù)據(jù)的話就會(huì)阻塞,除非它拿到 鎖。
什么是MVCC
Multi-Version Concurrency Control 多版本并發(fā)控制,MVCC 是一種并發(fā)控制的方法,一般在數(shù)據(jù)庫管理系統(tǒng)中,實(shí)現(xiàn)對(duì)數(shù)據(jù)庫的并發(fā)訪問;在編程語言中實(shí)現(xiàn)事務(wù)內(nèi)存。
多版本并發(fā)控制(MVCC) 是通過保存數(shù)據(jù)在某個(gè)時(shí)間點(diǎn)的快照來實(shí)現(xiàn)并發(fā)控制的,也就是說,不管事務(wù)執(zhí)行多長(zhǎng)時(shí)間,事務(wù)內(nèi)部看到的數(shù)據(jù)是不受其它事務(wù)影響的,根據(jù)事務(wù)開始的時(shí)間不同,每個(gè)事務(wù)對(duì)同一張表,同一時(shí)刻看到的數(shù)據(jù)可能是不一樣的。
簡(jiǎn)單來說,多版本并發(fā)控制的思想就是保存數(shù)據(jù)的歷史版本,通過對(duì)數(shù)據(jù)行的多個(gè)版本管理來實(shí)現(xiàn)數(shù)據(jù)庫的并發(fā)控制。這樣我們就可以通過比較版本號(hào)決定數(shù)據(jù)是否顯示出來,讀取數(shù)據(jù)的時(shí)候不需要加鎖也可以保證事務(wù)的隔離級(jí)別。
可以認(rèn)為多版本并發(fā)控制是行級(jí)鎖的一個(gè)變種,但是它在很多情況下避免了加鎖操作,因此開銷耕地。雖然實(shí)現(xiàn)機(jī)制有所不同,但大都實(shí)現(xiàn)類非阻塞的讀操作,寫操作也只鎖定必要的行。
MySQL的大多數(shù)事務(wù)性存儲(chǔ)引擎實(shí)現(xiàn)的都不是簡(jiǎn)單的行級(jí)鎖。基于提升并發(fā)性能的考慮,它們一般都同時(shí)實(shí)現(xiàn)了多版本并發(fā)控制。不僅是MySQL,包括Oracle、PostgreSQL等其它數(shù)據(jù)庫系統(tǒng)也都實(shí)現(xiàn)了MVCC,但各自的實(shí)現(xiàn)機(jī)制不盡相同,因?yàn)?em>MVCC沒有一個(gè)統(tǒng)一的實(shí)現(xiàn)標(biāo)準(zhǔn),典型的有樂觀并發(fā)控制和悲觀并發(fā)控制。
MVCC解決了什么問題
解決了在REPEATABLE READ和READ COMMITTED兩個(gè)隔離級(jí)別下讀同一行和寫同一行的兩個(gè)事務(wù)的并發(fā)。
-
讀寫直接阻塞的問題:通過 MVCC 可以讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,這樣就可以提升事務(wù)并發(fā)處理能力。
提高并發(fā)的演進(jìn)思路:
- 普通鎖,只能串行執(zhí)行;
- 讀寫鎖,可以實(shí)現(xiàn)讀讀并發(fā);
- 數(shù)據(jù)多版本并發(fā)控制,可以實(shí)現(xiàn)讀寫并發(fā)。
-
降低了死鎖的概率:因?yàn)?InnoDB 的 MVCC采用了樂觀鎖的方式,讀取數(shù)據(jù)時(shí)并不需要加鎖,對(duì)于寫操作,也只鎖定必要的行。
-
解決一致性讀的問題:一致性讀也被稱為快照讀,當(dāng)我們查詢數(shù)據(jù)庫在某個(gè)時(shí)間點(diǎn)的快照時(shí),只能看到這個(gè)時(shí)間點(diǎn)之前事務(wù)提交更新的結(jié)果,而不能看到這個(gè)時(shí)間點(diǎn)之后事務(wù)提交的更新結(jié)果。
舉個(gè)簡(jiǎn)單的例子:
- 一個(gè)事務(wù)A(txnId=100),修改了數(shù)據(jù)X,使得X=1,并且commit了。
- 另外一個(gè)事務(wù)B(txnId=101)開始嘗試讀取X,但是還X=1。但B沒有提交。
- 第三個(gè)事務(wù)C(txnId=102)修改了數(shù)據(jù)X,使得X=2,并且提交了。
- 事務(wù)B又一次讀取了X。這時(shí)
- 如果事務(wù)B是Read Committed,那么就讀取X的最新commit的版本,也就是X=2。
- 如果事務(wù)B是Repeatable Read。那么讀取的就是當(dāng)前事務(wù)(txnId=101)之前X的最新版本,也就是X被txnId=100提交的版本,即X=1。
注意,這里B不論是Read Committed,還是Repeatable Read,都不會(huì)被鎖,都能立刻拿到結(jié)果。這也就是MVCC存在的意義。
快照讀與當(dāng)前讀
快照讀(SnapShot Read)是一種一致性不加鎖的讀,是InnoDB并發(fā)如此之高的核心原因之一。
這里的一致性是指,事務(wù)讀取到的數(shù)據(jù),要么是事務(wù)開始前就已經(jīng)存在的數(shù)據(jù),要么是事務(wù)自身插入或者修改過的數(shù)據(jù)。
不加鎖的簡(jiǎn)單SELECT屬于快照讀,例如:
select * from users where id = '1';
與快照讀相對(duì)應(yīng)的則是當(dāng)前讀,當(dāng)前讀就是讀取最新數(shù)據(jù),而不是歷史版本的數(shù)據(jù)。加鎖的SELECT就屬于當(dāng)前讀,例如:
select * from users where id = '1' lock in share mode;
select * from users where id = '1' for update;
MVCC就是為了實(shí)現(xiàn)讀-寫沖突不加鎖,而這個(gè)讀指的就是快照讀,而非當(dāng)前讀,當(dāng)前讀實(shí)際上是一種加鎖的操作,是悲觀鎖的實(shí)現(xiàn)
InnoDB的MVCC是如何工作的
當(dāng)查詢一條記錄的時(shí)候,執(zhí)行流程如下:
- 首先獲取事務(wù)自己的版本號(hào),也就是事務(wù) ID;
- 獲取 Read View;
- 查詢得到的數(shù)據(jù),然后與 Read View 中的事務(wù)版本號(hào)進(jìn)行比較;
- 如果不符合 Read View 規(guī)則,就需要從 Undo Log 中獲取歷史快照;
- 最后返回符合規(guī)則的數(shù)據(jù)。
InnoDB是如何儲(chǔ)存記錄多個(gè)版本的
事務(wù)版本號(hào):
每開啟一個(gè)事務(wù),我們都會(huì)從數(shù)據(jù)庫中獲取一個(gè)事務(wù)ID(也就是事務(wù)版本號(hào)),這個(gè)事務(wù)ID是自增長(zhǎng)的,通過ID大小,我們就可以判斷事務(wù)的時(shí)間順序。
行記錄的隱藏列:
InnoDB的葉子段儲(chǔ)存了數(shù)據(jù)頁,數(shù)據(jù)頁中保存了行記錄,而在行記錄中有一些重要的隱藏字段:
- DB_ROW_ID:6-byte,隱藏的行ID,用來生成默認(rèn)聚簇索引。如果我們創(chuàng)建數(shù)據(jù)表的時(shí)候沒有指定聚簇索引,這時(shí)InnoDB就會(huì)用這個(gè)隱藏ID來創(chuàng)建聚簇索引。采用聚簇索引的方式可以提升數(shù)據(jù)的查找效率。
- DR_TRX_ID:6byte,操作這個(gè)數(shù)據(jù)的事務(wù)ID,也就是最后一個(gè)對(duì)該數(shù)據(jù)進(jìn)行插入或更新的事務(wù)ID。
- DB_ROLL_PTR:7byte,回滾指針,也就是指向這個(gè)記錄的Undo log信息。
Undo Log
InnoDB將行記錄保存在了Undo Log,我們就可以在回滾段中找到它們,如下圖所示:
從圖中能看到回滾指針將數(shù)據(jù)行的所有快照記錄都通過鏈表的結(jié)構(gòu)串聯(lián)了起來,每個(gè)快照的記錄都保存了當(dāng)時(shí)的db_trx_id,頁就是那個(gè)時(shí)間點(diǎn)操作這個(gè)數(shù)據(jù)的事務(wù)ID。這樣如果我們想找歷史數(shù)據(jù)快照,就可以通過遍歷回滾指針的方式進(jìn)行查找。
Read View(讀視圖)
什么是Read View,說白了Read View就是事務(wù)進(jìn)行快照讀操作的時(shí)候生產(chǎn)的讀視圖(Read View),在該事務(wù)執(zhí)行的快照讀的那一刻,會(huì)生成數(shù)據(jù)庫系統(tǒng)當(dāng)前的一個(gè)快照,記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)的ID(當(dāng)每個(gè)事務(wù)開啟時(shí),都會(huì)被分配一個(gè)ID, 這個(gè)ID是遞增的,所以最新的事務(wù),ID值越大)
所以我們知道Read View主要是用來做可見性判斷的,即當(dāng)我們某個(gè)事務(wù)只想快照讀的時(shí)候,對(duì)該記錄創(chuàng)建一個(gè)Read View讀視圖,把它比作條件用來判斷當(dāng)前事務(wù)是否能夠看到哪個(gè)版本的數(shù)據(jù),即可能是當(dāng)前最新的數(shù)據(jù),也有可能是該行記錄的undo log里面的的某個(gè)版本數(shù)據(jù)。
- trx_list 未提交事務(wù)ID列表,用來維護(hù)Read View生成時(shí)刻系統(tǒng)正活躍的事務(wù)ID
- up_limit_id 記錄trx_list列表中事務(wù)ID最小的ID
- low_limit_id ReadView生成時(shí)刻系統(tǒng)尚未分配的下一個(gè)事務(wù)ID,也就是目前已出現(xiàn)過的事務(wù)ID的最大值+1
- 首先比較DB_TRX_ID < up_limit_id, 如果小于,則當(dāng)前事務(wù)能看到DB_TRX_ID 所在的記錄,如果大于等于進(jìn)入下一個(gè)判斷
- 接下來判斷 DB_TRX_ID 大于等于 low_limit_id, 如果大于等于則代表DB_TRX_ID 所在的記錄在Read View生成后才出現(xiàn)的,那對(duì)當(dāng)前事務(wù)肯定不可見,如果小于則進(jìn)入下一個(gè)判斷
- 判斷DB_TRX_ID 是否在活躍事務(wù)之中,trx_list.contains(DB_TRX_ID),如果在,則代表我Read View生成時(shí)刻,你這個(gè)事務(wù)還在活躍,還沒有Commit,你修改的數(shù)據(jù),我當(dāng)前事務(wù)也是看不見的;如果不在,則說明,你這個(gè)事務(wù)在Read View生成之前就已經(jīng)Commit了,你修改的結(jié)果,我當(dāng)前事務(wù)是能看見的
在可重復(fù)讀(REPEATABLE READ)隔離級(jí)別下,InnoDB的MVCC是如何工作的
查詢
InnoDB會(huì)根據(jù)以下兩個(gè)條件檢查每行記錄:
- InnoDB只查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行(也就是,行的系統(tǒng)版本號(hào)小于或等于事務(wù)的系統(tǒng)版本號(hào)),這樣確保事務(wù)讀取的行,要么實(shí)在事務(wù)開始前已經(jīng)存在的,要么是事務(wù)自身插入或者修改過的
- 行的刪除版本要么未定義,要么大于當(dāng)前事務(wù)版本號(hào)。這樣可以確保事務(wù)讀取到的行,在事務(wù)開始之前未被刪除。
插入
InnoDB未新插入的每一行保存當(dāng)前系統(tǒng)號(hào)作為行版本號(hào)。
刪除
InnoDB為刪除的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為行刪除標(biāo)識(shí)。
刪除在內(nèi)部被視為更新,行中的一個(gè)特殊位會(huì)被設(shè)置為已刪除。
更新
InnoDB為插入一行新記錄,保存當(dāng)前系統(tǒng)版本號(hào)作為行版本號(hào),同時(shí)保存當(dāng)前系統(tǒng)版本號(hào)到原來的行作為行刪除標(biāo)識(shí)。
參考文章
MySQL - MySQL InnoDB的MVCC實(shí)現(xiàn)機(jī)制
MySQL的多版本并發(fā)控制(MVCC)是什么?
值得收藏,揭秘 MySQL 多版本并發(fā)控制實(shí)現(xiàn)原理
MVCC解決了什么問題?
MVCC實(shí)現(xiàn)原理之ReadView(一步到位)
MVCC百度百科
臟讀、不可重復(fù)讀的最終解決方案——MVCC文章來源:http://www.zghlxwxcb.cn/news/detail-800066.html
RR有幻讀問題嗎?MVCC能否解決幻讀?文章來源地址http://www.zghlxwxcb.cn/news/detail-800066.html
到了這里,關(guān)于MySQL 多版本并發(fā)控制 MVCC的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!