MVCC,全稱 Multi-Version Concurrency Control ,即多版本并發(fā)控制。MVCC 是一種并發(fā)控制的方法,一般在數(shù)據(jù)庫(kù)管理系統(tǒng)中,實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的并發(fā)訪問(wèn),在編程語(yǔ)言中實(shí)現(xiàn)事務(wù)內(nèi)存。
MVCC 在 MySQL InnoDB 中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫(kù)并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時(shí),也能做到不加鎖,非阻塞并發(fā)讀
像 select lock in share mode (共享鎖), select for update; update; insert; delete (排他鎖)這些操作都是一種當(dāng)前讀,為什么叫當(dāng)前讀?就是它讀取的是記錄的最新版本,讀取時(shí)還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會(huì)對(duì)讀取的記錄進(jìn)行加鎖
像不加鎖的 select 操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級(jí)別不是串行級(jí)別,串行級(jí)別下的快照讀會(huì)退化成當(dāng)前讀;之所以出現(xiàn)快照讀的情況,是基于提高并發(fā)性能的考慮,快照讀的實(shí)現(xiàn)是基于多版本并發(fā)控制,即 MVCC ,可以認(rèn)為 MVCC 是行鎖的一個(gè)變種,但它在很多情況下,避免了加鎖操作,降低了開(kāi)銷;既然是基于多版本,即快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本,而有可能是之前的歷史版本。
說(shuō)白了 MVCC 就是為了實(shí)現(xiàn)讀-寫沖突不加鎖,而這個(gè)讀指的就是快照讀, 而非當(dāng)前讀,當(dāng)前讀實(shí)際上是一種加鎖的操作,是悲觀鎖的實(shí)現(xiàn)
MVCC 多版本并發(fā)控制是 「維持一個(gè)數(shù)據(jù)的多個(gè)版本,使得讀寫操作沒(méi)有沖突」 的概念,只是一個(gè)抽象概念,并非實(shí)現(xiàn)
因?yàn)?MVCC 只是一個(gè)抽象概念,要實(shí)現(xiàn)這么一個(gè)概念,MySQL 就需要提供具體的功能去實(shí)現(xiàn)它,「快照讀就是 MySQL 實(shí)現(xiàn) MVCC 理想模型的其中一個(gè)非阻塞讀功能」。而相對(duì)而言,當(dāng)前讀就是悲觀鎖的具體功能實(shí)現(xiàn)
要說(shuō)的再細(xì)致一些,快照讀本身也是一個(gè)抽象概念,再深入研究。MVCC 模型在 MySQL 中的具體實(shí)現(xiàn)則是由 3 個(gè)隱式字段,undo 日志 , Read View 等去完成的,具體可以看下面的 MVCC 實(shí)現(xiàn)原理。
數(shù)據(jù)庫(kù)并發(fā)場(chǎng)景有三種,分別為:
讀-讀:不存在任何問(wèn)題,也不需要并發(fā)控制
讀-寫:有線程安全問(wèn)題,可能會(huì)造成事務(wù)隔離性問(wèn)題,可能遇到臟讀,幻讀,不可重復(fù)讀
寫-寫:有線程安全問(wèn)題,可能會(huì)存在更新丟失問(wèn)題,比如第一類更新丟失,第二類更新丟失
MVCC 帶來(lái)的好處是?
多版本并發(fā)控制(MVCC)是一種用來(lái)解決讀-寫沖突的無(wú)鎖并發(fā)控制,也就是為事務(wù)分配單向增長(zhǎng)的時(shí)間戳,為每個(gè)修改保存一個(gè)版本,版本與事務(wù)時(shí)間戳關(guān)聯(lián),讀操作只讀該事務(wù)開(kāi)始前的數(shù)據(jù)庫(kù)的快照。 所以 MVCC 可以為數(shù)據(jù)庫(kù)解決以下問(wèn)題:
在并發(fā)讀寫數(shù)據(jù)庫(kù)時(shí),可以做到在讀操作時(shí)不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數(shù)據(jù)庫(kù)并發(fā)讀寫的性能
同時(shí)還可以解決臟讀,幻讀,不可重復(fù)讀等事務(wù)隔離問(wèn)題,但不能解決更新丟失問(wèn)題
簡(jiǎn)而言之,MVCC 就是因?yàn)榇罄袀儯粷M意只讓數(shù)據(jù)庫(kù)采用悲觀鎖這樣性能不佳的形式去解決讀-寫沖突問(wèn)題,而提出的解決方案,所以在數(shù)據(jù)庫(kù)中,因?yàn)橛辛?MVCC,所以我們可以形成兩個(gè)組合:
MVCC + 悲觀鎖
MVCC解決讀寫沖突,悲觀鎖解決寫寫沖突
MVCC + 樂(lè)觀鎖
MVCC 解決讀寫沖突,樂(lè)觀鎖解決寫寫沖突
這種組合的方式就可以最大程度的提高數(shù)據(jù)庫(kù)并發(fā)性能,并解決讀寫沖突,和寫寫沖突導(dǎo)致的問(wèn)題。
MVCC 的目的就是多版本并發(fā)控制,在數(shù)據(jù)庫(kù)中的實(shí)現(xiàn),就是為了解決讀寫沖突,它的實(shí)現(xiàn)原理主要是依賴記錄中的 3個(gè)隱式字段,undo日志 , Read View 來(lái)實(shí)現(xiàn)的。所以我們先來(lái)看看這個(gè)三個(gè) point 的概念。
每行記錄除了我們自定義的字段外,還有數(shù)據(jù)庫(kù)隱式定義的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 等字段
DB_TRX_ID:
6 byte,最近修改(修改/插入)事務(wù) ID:記錄創(chuàng)建這條記錄/最后一次修改該記錄的事務(wù) ID
DB_ROLL_PTR:
7 byte,回滾指針,指向這條記錄的上一個(gè)版本(存儲(chǔ)于 rollback segment 里)
DB_ROW_ID:
6 byte,隱含的自增 ID(隱藏主鍵),如果數(shù)據(jù)表沒(méi)有主鍵,InnoDB 會(huì)自動(dòng)以DB_ROW_ID產(chǎn)生一個(gè)聚簇索引
實(shí)際還有一個(gè)刪除 flag 隱藏字段, 既記錄被更新或刪除并不代表真的刪除,而是刪除 flag 變了

如上圖,DB_ROW_ID 是數(shù)據(jù)庫(kù)默認(rèn)為該行記錄生成的唯一隱式主鍵,DB_TRX_ID 是當(dāng)前操作該記錄的事務(wù) ID ,而 DB_ROLL_PTR 是一個(gè)回滾指針,用于配合 undo日志,指向上一個(gè)舊版本
undo log 主要分為兩種:
insert undo log
代表事務(wù)在 insert 新記錄時(shí)產(chǎn)生的 undo log, 只在事務(wù)回滾時(shí)需要,并且在事務(wù)提交后可以被立即丟棄
update undo log
事務(wù)在進(jìn)行 update 或 delete 時(shí)產(chǎn)生的 undo log ; 不僅在事務(wù)回滾時(shí)需要,在快照讀時(shí)也需要;所以不能隨便刪除,只有在快速讀或事務(wù)回滾不涉及該日志時(shí),對(duì)應(yīng)的日志才會(huì)被 purge 線程統(tǒng)一清除
從前面的分析可以看出,為了實(shí)現(xiàn) InnoDB 的 MVCC 機(jī)制,更新或者刪除操作都只是設(shè)置一下老記錄的 deleted_bit ,并不真正將過(guò)時(shí)的記錄刪除。
為了節(jié)省磁盤空間,InnoDB 有專門的 purge 線程來(lái)清理 deleted_bit 為 true 的記錄。為了不影響 MVCC 的正常工作,purge 線程自己也維護(hù)了一個(gè)read view(這個(gè) read view 相當(dāng)于系統(tǒng)中最老活躍事務(wù)的 read view );如果某個(gè)記錄的 deleted_bit 為 true ,并且 DB_TRX_ID 相對(duì)于 purge 線程的 read view 可見(jiàn),那么這條記錄一定是可以被安全清除的。
對(duì) MVCC 有幫助的實(shí)質(zhì)是 update undo log ,undo log 實(shí)際上就是存在 rollback segment 中舊記錄鏈,它的執(zhí)行流程如下:
一、 比如一個(gè)有個(gè)事務(wù)插入 persion 表插入了一條新記錄,記錄如下,name 為 Jerry , age 為 24 歲,隱式主鍵是 1,事務(wù) ID 和回滾指針,我們假設(shè)為 NULL。

二、 現(xiàn)在來(lái)了一個(gè)事務(wù) 1 對(duì)該記錄的 name 做出了修改,改為 Tom
三、 又來(lái)了個(gè)事務(wù) 2 修改 person 表的同一個(gè)記錄,將age修改為 30 歲
從上面,我們就可以看出,不同事務(wù)或者相同事務(wù)的對(duì)同一記錄的修改,會(huì)導(dǎo)致該記錄的undo log成為一條記錄版本線性表,既鏈表,undo log 的鏈?zhǔn)拙褪亲钚碌呐f記錄,鏈尾就是最早的舊記錄(當(dāng)然就像之前說(shuō)的該 undo log 的節(jié)點(diǎn)可能是會(huì) purge 線程清除掉,向圖中的第一條 insert undo log,其實(shí)在事務(wù)提交之后可能就被刪除丟失了,不過(guò)這里為了演示,所以還放在這里)。
什么是 Read View,說(shuō)白了 Read View 就是事務(wù)進(jìn)行快照讀操作的時(shí)候生產(chǎn)的讀視圖 (Read View),在該事務(wù)執(zhí)行的快照讀的那一刻,會(huì)生成數(shù)據(jù)庫(kù)系統(tǒng)當(dāng)前的一個(gè)快照,記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)的 ID (當(dāng)每個(gè)事務(wù)開(kāi)啟時(shí),都會(huì)被分配一個(gè) ID , 這個(gè) ID 是遞增的,所以最新的事務(wù),ID 值越大)
所以我們知道 Read View 主要是用來(lái)做可見(jiàn)性判斷的, 即當(dāng)我們某個(gè)事務(wù)執(zhí)行快照讀的時(shí)候,對(duì)該記錄創(chuàng)建一個(gè) Read View 讀視圖,把它比作條件用來(lái)判斷當(dāng)前事務(wù)能夠看到哪個(gè)版本的數(shù)據(jù),既可能是當(dāng)前最新的數(shù)據(jù),也有可能是該行記錄的undo log里面的某個(gè)版本的數(shù)據(jù)。
Read View遵循一個(gè)可見(jiàn)性算法,主要是將要被修改的數(shù)據(jù)的最新記錄中的 DB_TRX_ID(即當(dāng)前事務(wù) ID )取出來(lái),與系統(tǒng)當(dāng)前其他活躍事務(wù)的 ID 去對(duì)比(由 Read View 維護(hù)),如果 DB_TRX_ID 跟 Read View 的屬性做了某些比較,不符合可見(jiàn)性,那就通過(guò) DB_ROLL_PTR 回滾指針去取出 Undo Log 中的 DB_TRX_ID 再比較,即遍歷鏈表的 DB_TRX_ID(從鏈?zhǔn)椎芥溛?,即從最近的一次修改查起),直到找到滿足特定條件的 DB_TRX_ID , 那么這個(gè) DB_TRX_ID 所在的舊記錄就是當(dāng)前事務(wù)能看見(jiàn)的最新老版本
那么這個(gè)判斷條件是什么呢?

該方法展示了我們拿 DB_TRX_ID 去跟 Read View 某些屬性進(jìn)行怎么樣的比較
可以把 Read View 簡(jiǎn)單的理解成有三個(gè)全局屬性:
up_limit_id
low_limit_id
3. hight water mark
ReadView 生成時(shí)刻系統(tǒng)尚未分配的下一個(gè)事務(wù) ID ,也就是 目前已出現(xiàn)過(guò)的事務(wù) ID 的最大值 + 1
為什么是 low_limit ? 因?yàn)樗彩窍到y(tǒng)此刻可分配的事務(wù) ID 的最小值
首先比較 DB_TRX_ID < up_limit_id , 如果小于,則當(dāng)前事務(wù)能看到 DB_TRX_ID 所在的記錄,如果大于等于進(jìn)入下一個(gè)判斷
接下來(lái)判斷 DB_TRX_ID >= low_limit_id , 如果大于等于則代表 DB_TRX_ID 所在的記錄在 Read View 生成后才出現(xiàn)的,那對(duì)當(dāng)前事務(wù)肯定不可見(jiàn),如果小于則進(jìn)入下一個(gè)判斷
判斷 DB_TRX_ID 是否在活躍事務(wù)之中,trx_list.contains (DB_TRX_ID),如果在,則代表我 Read View 生成時(shí)刻,你這個(gè)事務(wù)還在活躍,還沒(méi)有 Commit,你修改的數(shù)據(jù),我當(dāng)前事務(wù)也是看不見(jiàn)的;如果不在,則說(shuō)明,你這個(gè)事務(wù)在 Read View 生成之前就已經(jīng) Commit 了,你修改的結(jié)果,我當(dāng)前事務(wù)是能看見(jiàn)的
當(dāng)事務(wù) 2 對(duì)某行數(shù)據(jù)執(zhí)行了快照讀,數(shù)據(jù)庫(kù)為該行數(shù)據(jù)生成一個(gè)Read View讀視圖,假設(shè)當(dāng)前事務(wù) ID 為 2,此時(shí)還有事務(wù)1和事務(wù)3在活躍中,事務(wù) 4 在事務(wù) 2 快照讀前一刻提交更新了,所以 Read View 記錄了系統(tǒng)當(dāng)前活躍事務(wù) 1,3 的 ID,維護(hù)在一個(gè)列表上,假設(shè)我們稱為trx_list

Read View 不僅僅會(huì)通過(guò)一個(gè)列表 trx_list 來(lái)維護(hù)事務(wù) 2執(zhí)行快照讀那刻系統(tǒng)正活躍的事務(wù) ID 列表,還會(huì)有兩個(gè)屬性 up_limit_id( trx_list 列表中事務(wù) ID 最小的 ID ),low_limit_id ( 快照讀時(shí)刻系統(tǒng)尚未分配的下一個(gè)事務(wù) ID ,也就是目前已出現(xiàn)過(guò)的事務(wù)ID的最大值 + 1 資料傳送門 | 呵呵一笑百媚生的回答 ) 。所以在這里例子中 up_limit_id 就是1,low_limit_id 就是 4 + 1 = 5,trx_list 集合的值是 1, 3,Read View 如下圖

我們的例子中,只有事務(wù) 4 修改過(guò)該行記錄,并在事務(wù) 2 執(zhí)行快照讀前,就提交了事務(wù),所以當(dāng)前該行當(dāng)前數(shù)據(jù)的 undo log 如下圖所示;我們的事務(wù) 2 在快照讀該行記錄的時(shí)候,就會(huì)拿該行記錄的 DB_TRX_ID 去跟 up_limit_id , low_limit_id 和活躍事務(wù) ID 列表( trx_list )進(jìn)行比較,判斷當(dāng)前事務(wù) 2能看到該記錄的版本是哪個(gè)。

所以先拿該記錄 DB_TRX_ID 字段記錄的事務(wù) ID 4 去跟 Read View 的 up_limit_id 比較,看 4 是否小于 up_limit_id( 1 ),所以不符合條件,繼續(xù)判斷 4 是否大于等于 low_limit_id( 5 ),也不符合條件,最后判斷 4 是否處于 trx_list 中的活躍事務(wù), 最后發(fā)現(xiàn)事務(wù) ID 為 4 的事務(wù)不在當(dāng)前活躍事務(wù)列表中, 符合可見(jiàn)性條件,所以事務(wù) 4修改后提交的最新結(jié)果對(duì)事務(wù) 2 快照讀時(shí)是可見(jiàn)的,所以事務(wù) 2 能讀到的最新數(shù)據(jù)記錄是事務(wù)4所提交的版本,而事務(wù)4提交的版本也是全局角度上最新的版本

也正是 Read View 生成時(shí)機(jī)的不同,從而造成 RC , RR 級(jí)別下快照讀的結(jié)果的不同
摘抄自:https://blog.csdn.net/SnailMann/article/details/94724197

