1 什么是MVCC
MVCC (Multiversion Concurrency Control) 中文全稱叫多版本并發(fā)控制巫糙,是現(xiàn)代數(shù)據(jù)庫(包括 MySQL追迟、Oracle晃财、PostgreSQL 等)引擎實現(xiàn)中常用的處理讀寫沖突的手段,目的在于提高數(shù)據(jù)庫高并發(fā)場景下的吞吐性能穿仪。
如此一來不同的事務在并發(fā)過程中蜕依,SELECT 操作可以不加鎖而是通過 MVCC 機制讀取指定的版本歷史記錄搂赋,并通過一些手段保證保證讀取的記錄值符合事務所處的隔離級別塘慕,從而解決并發(fā)場景下的讀寫沖突。
下面舉一個多版本讀的例子戒傻,例如兩個事務 A 和 B 按照如下順序進行更新和讀取操作
在事務 A 提交前后税手,事務 B 讀取到的 x 的值是什么呢?答案是:事務 B 在不同的隔離級別下需纳,讀取到的值不一樣芦倒。
如果事務 B 的隔離級別是讀未提交(RU),那么兩次讀取均讀取到 x 的最新值候齿,即 20。
如果事務 B 的隔離級別是讀已提交(RC)闺属,那么第一次讀取到舊值 10慌盯,第二次因為事務 A 已經(jīng)提交,則讀取到新值 20掂器。
如果事務 B 的隔離級別是可重復讀或者串行(RR亚皂,S),則兩次均讀到舊值 10国瓮,不論事務 A 是否已經(jīng)提交灭必。
可見在不同的隔離級別下,數(shù)據(jù)庫通過 MVCC 和隔離級別乃摹,讓事務之間并行操作遵循了某種規(guī)則禁漓,來保證單個事務內前后數(shù)據(jù)的一致性。
2 為什么需要MVCC
InnoDB 相比 MyISAM 有兩大特點孵睬,一是支持事務而是支持行級鎖播歼,事務的引入帶來了一些新的挑戰(zhàn)。相對于串行處理來說掰读,并發(fā)事務處理能大大增加數(shù)據(jù)庫資源的利用率秘狞,提高數(shù)據(jù)庫系統(tǒng)的事務吞吐量,從而可以支持可以支持更多的用戶蹈集。但并發(fā)事務處理也會帶來一些問題烁试,主要包括以下幾種情況:
更新丟失(Lost Update):當兩個或多個事務選擇同一行,然后基于最初選定的值更新該行時拢肆,由于每個事務都不知道其他事務的存在减响,就會發(fā)生丟失更新問題 —— 最后的更新覆蓋了其他事務所做的更新靖诗。如何避免這個問題呢,最好在一個事務對數(shù)據(jù)進行更改但還未提交時辩蛋,其他事務不能訪問修改同一個數(shù)據(jù)呻畸。
臟讀(Dirty Reads):一個事務正在對一條記錄做修改,在這個事務并提交前悼院,這條記錄的數(shù)據(jù)就處于不一致狀態(tài)伤为;這時,另一個事務也來讀取同一條記錄据途,如果不加控制绞愚,第二個事務讀取了這些尚未提交的臟數(shù)據(jù),并據(jù)此做進一步的處理颖医,就會產生未提交的數(shù)據(jù)依賴關系位衩。這種現(xiàn)象被形象地叫做 “臟讀”。
不可重復讀(Non-Repeatable Reads):一個事務在讀取某些數(shù)據(jù)已經(jīng)發(fā)生了改變熔萧、或某些記錄已經(jīng)被刪除了糖驴!這種現(xiàn)象叫做“不可重復讀”。
幻讀(Phantom Reads):一個事務按相同的查詢條件重新讀取以前檢索過的數(shù)據(jù)佛致,卻發(fā)現(xiàn)其他事務插入了滿足其查詢條件的新數(shù)據(jù)贮缕,這種現(xiàn)象就稱為 “幻讀”。
以上是并發(fā)事務過程中會存在的問題俺榆,解決更新丟失可以交給應用感昼,但是后三者需要數(shù)據(jù)庫提供事務間的隔離機制來解決。實現(xiàn)隔離機制的方法主要有兩種:
加讀寫鎖
一致性快照讀罐脊,即 MVCC
但本質上定嗓,隔離級別是一種在并發(fā)性能和并發(fā)產生的副作用間的妥協(xié),通常數(shù)據(jù)庫均傾向于采用 Weak Isolation萍桌。
3 InnoDB MVCC實現(xiàn)原理
InnoDB
中 MVCC
的實現(xiàn)方式為:每一行記錄都有兩個隱藏列:DATA_TRX_ID
宵溅、DATA_ROLL_PTR
(如果沒有主鍵,則還會多一個隱藏的主鍵列)上炎。
DATA_TRX_ID
記錄最近更新這條行記錄的事務 ID
层玲,大小為 6
個字節(jié)
DATA_ROLL_PTR
表示指向該行回滾段(rollback segment)
的指針,大小為 7
個字節(jié)反症,InnoDB
便是通過這個指針找到之前版本的數(shù)據(jù)辛块。該行記錄上所有舊版本,在 undo
中都通過鏈表的形式組織铅碍。
DB_ROW_ID
行標識(隱藏單調自增 ID
)润绵,大小為 6
字節(jié),如果表沒有主鍵胞谈,InnoDB
會自動生成一個隱藏主鍵尘盼,因此會出現(xiàn)這個列憨愉。另外,每條記錄的頭信息(record header
)里都有一個專門的 bit
(deleted_flag
)來表示當前記錄是否已經(jīng)被刪除卿捎。
3.1 如何組織版本鏈
上文提到配紫,在多個事務并行操作某行數(shù)據(jù)的情況下,不同事務對該行數(shù)據(jù)的 UPDATE 會產生多個版本午阵,然后通過回滾指針組織成一條 Undo Log 鏈躺孝,這節(jié)我們通過一個簡單的例子來看一下 Undo Log 鏈是如何組織的,DATA_TRX_ID 和 DATA_ROLL_PTR 兩個參數(shù)在其中又起到什么樣的作用底桂。
還是以上文 MVCC 的例子植袍,事務 A 對值 x 進行更新之后,該行即產生一個新版本和舊版本籽懦。假設之前插入該行的事務 ID 為 100于个,事務 A 的 ID 為 200,該行的隱藏主鍵為 1暮顺。
事務 A 的操作過程為:
對 DB_ROW_ID = 1 的這行記錄加排他鎖
把該行原本的值拷貝到 undo log 中厅篓,DB_TRX_ID 和 DB_ROLL_PTR 都不動
修改該行的值這時產生一個新版本,更新 DATA_TRX_ID 為修改記錄的事務 ID捶码,將 DATA_ROLL_PTR 指向剛剛拷貝到 undo log 鏈中的舊版本記錄羽氮,這樣就能通過 DB_ROLL_PTR 找到這條記錄的歷史版本。如果對同一行記錄執(zhí)行連續(xù)的 UPDATE宙项,Undo Log 會組成一個鏈表乏苦,遍歷這個鏈表可以看到這條記錄的變遷
記錄 redo log株扛,包括 undo log 中的修改
那么 INSERT 和 DELETE 會怎么做呢尤筐?其實相比 UPDATE 這二者很簡單,INSERT 會產生一條新紀錄洞就,它的 DATA_TRX_ID 為當前插入記錄的事務 ID盆繁;DELETE 某條記錄時可看成是一種特殊的 UPDATE,其實是軟刪旬蟋,真正執(zhí)行刪除操作會在 commit 時油昂,DATA_TRX_ID 則記錄下刪除該記錄的事務 ID。
3.2 如何實現(xiàn)一致性讀-ReadView
在 RU 隔離級別下倾贰,直接讀取版本的最新記錄就 OK冕碟,對于 SERIALIZABLE 隔離級別,則是通過加鎖互斥來訪問數(shù)據(jù)匆浙,因此不需要 MVCC 的幫助安寺。因此 MVCC 運行在 RC 和 RR這兩個隔離級別下,當 InnoDB 隔離級別設置為二者其一時首尼,在 SELECT 數(shù)據(jù)時就會用到版本鏈
核心問題是版本鏈中哪些版本對當前事務可見挑庶?
InnoDB 為了解決這個問題言秸,設計了 ReadView(可讀視圖)的概念。
3.2.1 RR下ReadView的生成
在 RR 隔離級別下迎捺,每個事務 touch first read 時(本質上就是執(zhí)行第一個 SELECT語句時举畸,后續(xù)所有的 SELECT 都是復用這個 ReadView,其它 update, delete, insert 語句和一致性讀 snapshot 的建立沒有關系)凳枝,會將當前系統(tǒng)中的所有的活躍事務拷貝到一個列表生成 ReadView抄沮。
下圖中事務 A 第一條 SELECT 語句在事務 B 更新數(shù)據(jù)前,因此生成的 ReadView 在事務 A 過程中不發(fā)生變化范舀,即使事務 B 在事務 A 之前提交合是,但是事務 A 第二條查詢語句依舊無法讀到事務 B 的修改。
下圖中锭环,事務 A 的第一條 SELECT 語句在事務 B 的修改提交之后聪全,因此可以讀到事務 B的修改。但是注意辅辩,如果事務 A 的第一條 SELECT 語句查詢時难礼,事務 B 還未提交,那么事務 A 也查不到事務 B 的修改玫锋。
下圖中蛾茉,事務 A 的第一條 SELECT 語句在事務 B 的修改提交之后,因此可以讀到事務 B的修改撩鹿。但是注意谦炬,如果事務 A 的第一條 SELECT 語句查詢時,事務 B 還未提交节沦,那么事務 A 也查不到事務 B 的修改键思。
3.2.2 RC下ReadView的生成
在 RC 隔離級別下,每個 SELECT 語句開始時甫贯,都會重新將當前系統(tǒng)中的所有的活躍事務拷貝到一個列表生成 ReadView吼鳞。二者的區(qū)別就在于生成 ReadView 的時間點不同,一個是事務之后第一個 SELECT 語句開始叫搁、一個是事務中每條 SELECT 語句開始赔桌。
ReadView 中是當前活躍的事務 ID 列表,稱之為 m_ids渴逻,其中最小值為 up_limit_id疾党,最大值為 low_limit_id,事務 ID 是事務開啟時 InnoDB 分配的惨奕,其大小決定了事務開啟的先后順序雪位,因此我們可以通過 ID 的大小關系來決定版本記錄的可見性,具體判斷流程如下:
如果被訪問版本的 trx_id 小于 m_ids 中的最小值 up_limit_id墓贿,說明生成該版本的事務在 ReadView 生成前就已經(jīng)提交了茧泪,所以該版本可以被當前事務訪問蜓氨。
如果被訪問版本的 trx_id 大于 m_ids 列表中的最大值 low_limit_id,說明生成該版本的事務在生成 ReadView 后才生成队伟,所以該版本不可以被當前事務訪問穴吹。需要根據(jù) Undo Log 鏈找到前一個版本,然后根據(jù)該版本的 DB_TRX_ID 重新判斷可見性嗜侮。
如果被訪問版本的 trx_id 屬性值在 m_ids 列表中最大值和最小值之間(包含)港令,那就需要判斷一下 trx_id 的值是不是在 m_ids 列表中。如果在锈颗,說明創(chuàng)建 ReadView 時生成該版本所屬事務還是活躍的顷霹,因此該版本不可以被訪問,需要查找 Undo Log 鏈得到上一個版本击吱,然后根據(jù)該版本的 DB_TRX_ID 再從頭計算一次可見性淋淀;如果不在,說明創(chuàng)建 ReadView 時生成該版本的事務已經(jīng)被提交覆醇,該版本可以被訪問朵纷。
此時經(jīng)過一系列判斷我們已經(jīng)得到了這條記錄相對 ReadView 來說的可見結果。此時永脓,如果這條記錄的 delete_flag 為 true袍辞,說明這條記錄已被刪除,不返回常摧。否則說明此記錄可以安全返回給客戶端搅吁。
4 舉個例子
4.1 RC下的MVCC判斷流程
我們現(xiàn)在回看剛剛的查詢過程,為什么事務 B 在 RC 隔離級別下落午,兩次查詢的 x 值不同谎懦。RC 下 ReadView 是在語句粒度上生成的。
當事務 A 未提交時板甘,事務 B 進行查詢党瓮,假設事務 B 的事務 ID 為 300详炬,此時生成 ReadView 的 m_ids 為 [200盐类,300],而最新版本的 trx_id 為 200呛谜,處于 m_ids中在跳,則該版本記錄不可被訪問,查詢版本鏈得到上一條記錄的 trx_id 為 100隐岛,小于 m_ids的最小值 200猫妙,因此可以被訪問,此時事務 B 就查詢到值 10 而非 20聚凹。
待事務 A 提交之后割坠,事務 B 進行查詢齐帚,此時生成的 ReadView 的 m_ids 為 [300],而最新的版本記錄中 trx_id 為 200彼哼,小于 m_ids 的最小值 300对妄,因此可以被訪問到,此時事務 B 就查詢到 20敢朱。
4.2 RR下的MVCC判斷流程
如果在 RR 隔離級別下剪菱,為什么事務 B 前后兩次均查詢到 10 呢?RR 下生成 ReadView 是在事務開始時拴签,m_ids 為 [200,300]孝常,后面不發(fā)生變化,因此即使事務 A 提交了蚓哩,trx_id 為 200 的記錄依舊處于 m_ids 中构灸,不能被訪問,只能訪問版本鏈中的記錄 10岸梨。
5 一個爭論點
其實并非所有的情況都能套用 MVCC 讀的判斷流程冻押,特別是針對在事務進行過程中,另一個事務已經(jīng)提交修改的情況下盛嘿,這時不論是 RC 還是 RR洛巢,直接套用 MVCC 判斷都會有問題,例如 RC 下:
事務 A 的 trx_id = 200次兆,事務 B 的 trx_id = 300稿茉,且事務 B 修改了數(shù)據(jù)之后在事務 A 之前提交,此時 RC 下事務 A 讀到的數(shù)據(jù)為事務 B 修改后的值芥炭,這是很顯然的漓库。下面我們套用下 MVCC 的判斷流程,考慮到事務 A 第二次 SELECT 時园蝠,m_ids 應該為 [200]渺蒿,此時該行數(shù)據(jù)最新的版本 DATA_TRX_ID = 300 比 200 大,照理應該不能被訪問彪薛,但實際上事務 A 選取了這條記錄返回茂装。
這里其實應該結合 RC 的本質來看,RC 的本質就是事務中每一條 SELECT 語句均可以看到其他已提交事務對數(shù)據(jù)的修改善延,那么只要該事物已經(jīng)提交其結果就是可見的少态,與這兩個事務開始的先后順序無關,不完全適用于 MVCC 讀易遣。
RR 級別下還是用之前那張圖:
這張圖的流程中彼妻,事務 B 的 trx_id = 300 比事務 A 200 小,且事務 B 先于事務 A 提交,按照 MVCC 的判斷流程侨歉,事務 A 生成的 ReadView 為 [200]屋摇,最新版本的行記錄 DATA_TRX_ID = 300 比 200 大,照理不能訪問到幽邓,但是事務 A 實際上讀到了事務 B 已經(jīng)提交的修改摊册。這里還是結合 RR 本質進行解釋,RR 的本質是從第一個 SELECT 語句生成 ReadView 開始颊艳,任何已經(jīng)提交過的事務的修改均可見茅特。
6 寫在最后
RC、RR 兩種隔離級別的事務在執(zhí)行普通的讀操作時棋枕,通過訪問版本鏈的方法白修,使得事務間的讀寫操作得以并發(fā)執(zhí)行,從而提升系統(tǒng)性能重斑。RC兵睛、RR 這兩個隔離級別的一個很大不同就是生成 ReadView 的時間點不同,RC 在每一次 SELECT 語句前都會生成一個 ReadView窥浪,事務期間會更新祖很,因此在其他事務提交前后所得到的 m_ids 列表可能發(fā)生變化,使得先前不可見的版本后續(xù)又突然可見了漾脂。而 RR 只在事務的第一個 SELECT 語句時生成一個 ReadView假颇,事務操作期間不更新。