前言 MVCC實(shí)現(xiàn)原理是一道非常高頻的面試題,最近技術(shù)討論群的小伙伴一直在討論,趁著國慶節(jié)有空,我們一起來聊聊。
1. 相關(guān)數(shù)據(jù)庫知識(shí)點(diǎn)回顧 1.1 什么是數(shù)據(jù)庫事務(wù),為什么要有事務(wù)事務(wù),由一個(gè)有限的數(shù)據(jù)庫操作序列構(gòu)成,這些操作要么全部執(zhí)行,要么全部不執(zhí)行,是一個(gè)不可分割的工作單位。
假如A轉(zhuǎn)賬給B 100 元,先從A的賬戶里扣除 100 元,再在 B 的賬戶上加上 100 元。如果扣完A的100元后,還沒來得及給B加上,銀行系統(tǒng)異常了,最后導(dǎo)致A的余額減少了,B的余額卻沒有增加。所以就需要事務(wù),將A的錢回滾回去,就是這么簡單。 為什么要有事務(wù)呢? 就是為了保證數(shù)據(jù)的最終一致性。
1.2 事務(wù)包括哪幾個(gè)特性?事務(wù)四個(gè)典型特性,即ACID,原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)。
原子性:事務(wù)作為一個(gè)整體被執(zhí)行,包含在其中的對數(shù)據(jù)庫的操作要么全部都執(zhí)行,要么都不執(zhí)行。 一致性:指在事務(wù)開始之前和事務(wù)結(jié)束以后,數(shù)據(jù)不會(huì)被破壞,假如A賬戶給B賬戶轉(zhuǎn)10塊錢,不管成功與否,A和B的總金額是不變的。 隔離性:多個(gè)事務(wù)并發(fā)訪問時(shí),事務(wù)之間是相互隔離的,一個(gè)事務(wù)不應(yīng)該被其他事務(wù)干擾,多個(gè)并發(fā)事務(wù)之間要相互隔離。。 持久性:表示事務(wù)完成提交后,該事務(wù)對數(shù)據(jù)庫所作的操作更改,將持久地保存在數(shù)據(jù)庫之中。 1.3 事務(wù)并發(fā)存在的問題事務(wù)并發(fā)會(huì)引起
臟讀、不可重復(fù)讀、幻讀 問題。
1.3.1 臟讀如果一個(gè)事務(wù)讀取到了另一個(gè)未提交事務(wù)修改過的數(shù)據(jù),我們就稱發(fā)生了臟讀現(xiàn)象。 假設(shè)現(xiàn)在有兩個(gè)事務(wù)A、B:
假設(shè)現(xiàn)在A的余額是100,事務(wù)A正在準(zhǔn)備查詢Jay的余額 事務(wù)B先扣減Jay的余額,扣了10,但是還沒提交 最后A讀到的余額是90,即扣減后的余額 臟讀 因?yàn)槭聞?wù)A讀取到事務(wù)B
未提交的數(shù)據(jù) ,這就是臟讀。
1.3.2 不可重復(fù)讀同一個(gè)事務(wù)內(nèi),前后多次讀取,讀取到的數(shù)據(jù)內(nèi)容不一致 假設(shè)現(xiàn)在有兩個(gè)事務(wù)A和B:
事務(wù)A先查詢Jay的余額,查到結(jié)果是100 這時(shí)候事務(wù)B 對Jay的賬戶余額進(jìn)行扣減,扣去10后,提交事務(wù) 事務(wù)A再去查詢Jay的賬戶余額發(fā)現(xiàn)變成了90 不可重復(fù)讀 事務(wù)A被事務(wù)B干擾到了!在事務(wù)A范圍內(nèi),兩個(gè)相同的查詢,讀取同一條記錄,卻返回了不同的數(shù)據(jù),這就是
不可重復(fù)讀 。
1.3.3 幻讀如果一個(gè)事務(wù)先根據(jù)某些搜索條件查詢出一些記錄,在該事務(wù)未提交時(shí),另一個(gè)事務(wù)寫入了一些符合那些搜索條件的記錄(如insert、delete、update),就意味著發(fā)生了幻讀 。 假設(shè)現(xiàn)在有兩個(gè)事務(wù)A、B:
事務(wù)A先查詢id大于2的賬戶記錄,得到記錄id=2和id=3的兩條記錄 這時(shí)候,事務(wù)B開啟,插入一條id=4的記錄,并且提交了 事務(wù)A再去執(zhí)行相同的查詢,卻得到了id=2,3,4的3條記錄了。 幻讀 事務(wù)A查詢一個(gè)范圍的結(jié)果集,另一個(gè)并發(fā)事務(wù)B往這個(gè)范圍中插入新的數(shù)據(jù),并提交事務(wù),然后事務(wù)A再次查詢相同的范圍,兩次讀取到的結(jié)果集卻不一樣了,這就是幻讀。
1.4 四大隔離級(jí)別為了解決并發(fā)事務(wù)存在的
臟讀、不可重復(fù)讀、幻讀 等問題,數(shù)據(jù)庫大叔設(shè)計(jì)了四種隔離級(jí)別。分別是
讀未提交,讀已提交,可重復(fù)讀,串行化(Serializable) 。
1.4.1 讀未提交讀未提交隔離級(jí)別,只限制了兩個(gè)數(shù)據(jù)
不能同時(shí)修改 ,但是修改數(shù)據(jù)的時(shí)候,即使事務(wù)
未提交 ,都是可以被別的事務(wù)讀取到的,這級(jí)別的事務(wù)隔離有
臟讀、重復(fù)讀、幻讀 的問題;
1.4.2 讀已提交讀已提交隔離級(jí)別,當(dāng)前事務(wù)只能讀取到其他事務(wù)
提交 的數(shù)據(jù),所以這種事務(wù)的隔離級(jí)別
解決了臟讀 問題,但還是會(huì)存在
重復(fù)讀、幻讀 問題;
1.4 3 可重復(fù)讀可重復(fù)讀隔離級(jí)別,限制了讀取數(shù)據(jù)的時(shí)候,不可以進(jìn)行修改,所以
解決了重復(fù)讀 的問題,但是讀取范圍數(shù)據(jù)的時(shí)候,是可以插入數(shù)據(jù),所以還會(huì)存在
幻讀 問題;
1.4.4 串行化事務(wù)最高的隔離級(jí)別,在該級(jí)別下,所有事務(wù)都是進(jìn)行
串行化順序 執(zhí)行的。可以避免臟讀、不可重復(fù)讀與幻讀所有并發(fā)問題。但是這種事務(wù)隔離級(jí)別下,事務(wù)執(zhí)行很耗性能。
1.4.5 四大隔離級(jí)別,都會(huì)存在哪些并發(fā)問題呢隔離級(jí)別 臟讀 不可重復(fù)讀 幻讀 讀未提交 √ √ √ 讀已提交 × √ √ 可重復(fù)讀 × × √ 串行化 × × ×
1.5 數(shù)據(jù)庫是如何保證事務(wù)的隔離性的呢?數(shù)據(jù)庫是通過
加鎖 ,來實(shí)現(xiàn)事務(wù)的隔離性的。這就好像,如果你想一個(gè)人靜靜,不被別人打擾,你就可以在房門上加上一把鎖。加鎖確實(shí)好使,可以保證隔離性。比如
串行化隔離級(jí)別就是加鎖實(shí)現(xiàn)的 。但是頻繁的加鎖,導(dǎo)致讀數(shù)據(jù)時(shí),沒辦法修改,修改數(shù)據(jù)時(shí),沒辦法讀取,大大
降低了數(shù)據(jù)庫性能 。
那么,如何解決加鎖后的性能問題的? 答案就是,
MVCC多版本并發(fā)控制 !它實(shí)現(xiàn)讀取數(shù)據(jù)不用加鎖,可以讓讀取數(shù)據(jù)同時(shí)修改。修改數(shù)據(jù)時(shí)同時(shí)可讀取。
2. 什么是 MVCC? MVCC,即
Multi-Version ?Concurrency Control (多版本并發(fā)控制) 。它是一種并發(fā)控制的方法,一般在數(shù)據(jù)庫管理系統(tǒng)中,實(shí)現(xiàn)對數(shù)據(jù)庫的并發(fā)訪問,在編程語言中實(shí)現(xiàn)事務(wù)內(nèi)存。
通俗的講,數(shù)據(jù)庫中同時(shí)存在多個(gè)版本的數(shù)據(jù),并不是整個(gè)數(shù)據(jù)庫的多個(gè)版本,而是某一條記錄的多個(gè)版本同時(shí)存在,在某個(gè)事務(wù)對其進(jìn)行操作的時(shí)候,需要查看這一條記錄的隱藏列事務(wù)版本id,比對事務(wù)id并根據(jù)事物隔離級(jí)別去判斷讀取哪個(gè)版本的數(shù)據(jù)。 數(shù)據(jù)庫隔離級(jí)別讀
已提交、可重復(fù)讀 都是基于MVCC實(shí)現(xiàn)的,相對于加鎖簡單粗暴的方式,它用更好的方式去處理讀寫沖突,能有效提高數(shù)據(jù)庫并發(fā)性能。
3. MVCC實(shí)現(xiàn)的關(guān)鍵知識(shí)點(diǎn) 3.1 事務(wù)版本號(hào)事務(wù)每次開啟前,都會(huì)從數(shù)據(jù)庫獲得一個(gè)自增 長的事務(wù)ID,可以從事務(wù)ID判斷事務(wù)的執(zhí)行先后順序。這就是事務(wù)版本號(hào)。 3.2 隱式字段對于InnoDB存儲(chǔ)引擎,每一行記錄都有兩個(gè)隱藏列
trx_id 、
roll_pointer ,如果表中沒有主鍵和非NULL唯一鍵時(shí),則還會(huì)有第三個(gè)隱藏的主鍵列
row_id 。
列名 是否必須 描述 row_id 否 單調(diào)遞增的行ID,不是必需的,占用6個(gè)字節(jié)。 trx_id 是 記錄操作該數(shù)據(jù)事務(wù)的事務(wù)ID roll_pointer 是 這個(gè)隱藏列就相當(dāng)于一個(gè)指針,指向回滾段的undo日志
3.3 undo logundo log,
回滾日志 ,用于記錄數(shù)據(jù)被修改前的信息。在表記錄修改之前,會(huì)先把數(shù)據(jù)拷貝到undo log里,如果事務(wù)回滾,即可以通過undo log來還原數(shù)據(jù)。
可以這樣認(rèn)為,當(dāng)delete一條記錄時(shí),undo log 中會(huì)記錄一條對應(yīng)的insert記錄,當(dāng)update一條記錄時(shí),它記錄一條對應(yīng)相反的update記錄。undo log有什么
用途 呢?
事務(wù)回滾時(shí),保證原子性和一致性。 用于MVCC快照讀 。 3.4 版本鏈多個(gè)事務(wù)并行操作某一行數(shù)據(jù)時(shí),不同事務(wù)對該行數(shù)據(jù)的修改會(huì)產(chǎn)生多個(gè)版本,然后通過回滾指針(roll_pointer),連成一個(gè)鏈表,這個(gè)鏈表就稱為
版本鏈 。如下:
版本鏈 其實(shí),通過版本鏈,我們就可以看出
事務(wù)版本號(hào)、表格隱藏的列和undo log 它們之間的關(guān)系。我們再來小分析一下。
假設(shè)現(xiàn)在有一張core_user表,表里面有一條數(shù)據(jù),id為1,名字為孫權(quán): 現(xiàn)在開啟一個(gè)事務(wù)A:對core_user表執(zhí)行update core_user set name ="曹操" where id=1
,會(huì)進(jìn)行如下流程操作 首先獲得一個(gè)事務(wù)ID=100 把core_user表修改前的數(shù)據(jù),拷貝到undo log 修改core_user表中,id=1的數(shù)據(jù),名字改為曹操 把修改后的數(shù)據(jù)事務(wù)Id=101改成當(dāng)前事務(wù)版本號(hào),并把roll_pointer 指向undo log數(shù)據(jù)地址。 3.5 快照讀和當(dāng)前讀快照讀: 讀取的是記錄數(shù)據(jù)的可見版本(有舊的版本)。不加鎖,普通的select語句都是快照讀,如:
select?*?from?core_user?where ?id?>?2;
當(dāng)前讀 :讀取的是記錄數(shù)據(jù)的最新版本,顯式加鎖的都是當(dāng)前讀
select?*?from?core_user?where ?id?>?2?for ?update; select?*?from?account?where ?id>2?lock?in ?share?mode;
3.6 Read ViewRead View是什么呢? 它就是事務(wù)執(zhí)行SQL語句時(shí),產(chǎn)生的讀視圖。實(shí)際上在innodb中,每個(gè)SQL語句執(zhí)行前都會(huì)得到一個(gè)Read View。Read View有什么用呢? 它主要是用來做可見性判斷的,即判斷當(dāng)前事務(wù)可見哪個(gè)版本的數(shù)據(jù)~Read View是如何保證可見性判斷的呢?我們先看看Read view 的幾個(gè)重要屬性
m_ids:當(dāng)前系統(tǒng)中那些活躍(未提交)的讀寫事務(wù)ID, 它數(shù)據(jù)結(jié)構(gòu)為一個(gè)List。 min_limit_id:表示在生成Read View時(shí),當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù)id,即m_ids中的最小值。 max_limit_id:表示生成Read View時(shí),系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的id值。 creator_trx_id: 創(chuàng)建當(dāng)前Read?View的事務(wù)ID Read view 匹配條件規(guī)則 如下:
如果數(shù)據(jù)事務(wù)ID trx_id < min_limit_id
,表明生成該版本的事務(wù)在生成Read View前,已經(jīng)提交(因?yàn)槭聞?wù)ID是遞增的),所以該版本可以被當(dāng)前事務(wù)訪問。 如果trx_id>= max_limit_id
,表明生成該版本的事務(wù)在生成ReadView后才生成,所以該版本不可以被當(dāng)前事務(wù)訪問。 如果 min_limit_id =,需腰分3種情況討論
(1).如果m_ids
包含trx_id
,則代表Read View生成時(shí)刻,這個(gè)事務(wù)還未提交,但是如果數(shù)據(jù)的trx_id
等于creator_trx_id
的話,表明數(shù)據(jù)是自己生成的,因此是可見 的。 (2)如果m_ids
包含trx_id
,并且trx_id
不等于creator_trx_id
,則Read ? View生成時(shí),事務(wù)未提交,并且不是自己生產(chǎn)的,所以當(dāng)前事務(wù)也是看不見 的; (3).如果m_ids
不包含trx_id
,則說明你這個(gè)事務(wù)在Read View生成之前就已經(jīng)提交了,修改的結(jié)果,當(dāng)前事務(wù)是能看見的。 4. MVCC實(shí)現(xiàn)原理分析 4.1 查詢一條記錄,基于MVCC,是怎樣的流程獲取事務(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 實(shí)現(xiàn)MVCC,是通過
Read View Undo Log
實(shí)現(xiàn)的,Undo Log 保存了歷史快照,Read View可見性規(guī)則幫助判斷當(dāng)前版本的數(shù)據(jù)是否可見。
4.2 讀已提交(RC)隔離級(jí)別,存在不可重復(fù)讀問題的分析歷程創(chuàng)建core_user表,插入一條初始化數(shù)據(jù),如下: 隔離級(jí)別設(shè)置為讀已提交(RC),事務(wù)A和事務(wù)B同時(shí)對core_user表進(jìn)行查詢和修改操作。 事務(wù)A:?select?*?fom?core_user?where ?id=1 事務(wù)B:?update?core_user?set ?name?=”曹操”
執(zhí)行流程如下:
最后事務(wù)A查詢到的結(jié)果是,
name=曹操 的記錄,我們
基于MVCC ,來分析一下執(zhí)行流程:(1). A開啟事務(wù),首先得到一個(gè)事務(wù)ID為100(2).B開啟事務(wù),得到事務(wù)ID為101(3).事務(wù)A生成一個(gè)Read View,read view對應(yīng)的值如下
變量 值 m_ids 100,101 max_limit_id 102 min_limit_id 100 creator_trx_id 100
然后回到版本鏈:開始從版本鏈中挑選可見的記錄:
版本鏈 由圖可以看出,最新版本的列name的內(nèi)容是
孫權(quán)
,該版本的
trx_id
值為100。開始執(zhí)行read view可見性規(guī)則校驗(yàn):
min_limit_id(100)=creator_trx_id?=?trx_id?=100;
由此可得,trx_id=100的這個(gè)記錄,當(dāng)前事務(wù)是可見的。所以查到是name為
孫權(quán)
的記錄。(4). 事務(wù)B進(jìn)行修改操作,把名字改為曹操。把原數(shù)據(jù)拷貝到undo log,然后對數(shù)據(jù)進(jìn)行修改,標(biāo)記事務(wù)ID和上一個(gè)數(shù)據(jù)版本在undo log的地址。
(5) 提交事務(wù)(6) 事務(wù)A再次執(zhí)行查詢操作,
新生成一個(gè)Read View ,Read View對應(yīng)的值如下
變量 值 m_ids 100 max_limit_id 102 min_limit_id 100 creator_trx_id 100
然后再次回到版本鏈:從版本鏈中挑選可見的記錄:
從圖可得,最新版本的列name的內(nèi)容是
曹操
,該版本的
trx_id
值為101。開始執(zhí)行Read View可見性規(guī)則校驗(yàn):
min_limit_id(100)=但是,trx_id=101,不屬于m_ids集合
因此,
trx_id=101
這個(gè)記錄,對于當(dāng)前事務(wù)是可見的。所以SQL查詢到的是name為
曹操
的記錄。綜上所述,在
讀已提交(RC)隔離級(jí)別 下,同一個(gè)事務(wù)里,兩個(gè)相同的查詢,讀取同一條記錄(id=1),卻返回了不同的數(shù)據(jù)(
第一次查出來是孫權(quán),第二次查出來是曹操那條記錄 ),因此RC隔離級(jí)別,存在
不可重復(fù)讀 并發(fā)問題。
4.3 可重復(fù)讀(RR)隔離級(jí)別,解決不可重復(fù)讀問題的分析在RR隔離級(jí)別下,是如何解決不可重復(fù)讀問題的呢?我們一起再來看下,還是4.2小節(jié)那個(gè)流程,還是這個(gè)事務(wù)A和事務(wù)B,如下:
4.3.1 不同隔離級(jí)別下,Read View的工作方式不同實(shí)際上,各種事務(wù)隔離級(jí)別下的Read view工作方式,是不一樣的,RR可以解決不可重復(fù)讀問題,就是跟
Read view工作方式有關(guān) 。
在讀已提交(RC)隔離級(jí)別下,同一個(gè)事務(wù)里面,每一次查詢都會(huì)產(chǎn)生一個(gè)新的Read View副本 ,這樣就可能造成同一個(gè)事務(wù)里前后讀取數(shù)據(jù)可能不一致的問題(不可重復(fù)讀并發(fā)問題)。 begin select * from core_user where id =1 生成一個(gè)Read View / / / / select * from core_user where id =1 生成一個(gè)Read View
在可重復(fù)讀(RR)隔離級(jí)別下,一個(gè)事務(wù)里只會(huì)獲取一次read view ,都是副本共用的,從而保證每次查詢的數(shù)據(jù)都是一樣的。 begin select * from core_user where id =1 生成一個(gè)Read View / / select * from core_user where id =1 共用一個(gè)Read View副本
4.3.2 實(shí)例分析我們穿越下,回到
剛4.2的例子 ,然后執(zhí)行第2個(gè)查詢的時(shí)候:事務(wù)A再次執(zhí)行查詢操作,復(fù)用老的Read View副本,Read View對應(yīng)的值如下
變量 值 m_ids 100,101 max_limit_id 102 min_limit_id 100 creator_trx_id 100
然后再次回到版本鏈:從版本鏈中挑選可見的記錄:
從圖可得,最新版本的列name的內(nèi)容是
曹操
,該版本的
trx_id
值為101。開始執(zhí)行read view可見性規(guī)則校驗(yàn):
min_limit_id(100)=因?yàn)閙_ids{100,101}包含trx_id(101), 并且creator_trx_id?(100)?不等于trx_id(101)
所以,
trx_id=101
這個(gè)記錄,對于當(dāng)前事務(wù)是
不可見 的。這時(shí)候呢,版本鏈
roll_pointer
跳到下一個(gè)版本,
trx_id=100
這個(gè)記錄,再次校驗(yàn)是否可見:
min_limit_id(100)=因?yàn)閙_ids{100,101}包含trx_id(100), 并且creator_trx_id?(100)?等于trx_id(100)
所以,
trx_id=100
這個(gè)記錄,對于當(dāng)前事務(wù)是
可見 的,所以兩次查詢結(jié)果,都是
name=孫權(quán) 的那個(gè)記錄。即在可重復(fù)讀(RR)隔離級(jí)別下,復(fù)用老的Read View副本,解決了
不可重復(fù)讀 的問題。
4.4 網(wǎng)絡(luò)江湖傳說,MVCC是否解決了幻讀問題呢?網(wǎng)絡(luò)江湖有個(gè)傳說,說MVCC的RR隔離級(jí)別,解決了幻讀問題,我們來一起分析一下。
4.4.1 RR級(jí)別下,一個(gè)快照讀的例子,不存在幻讀問題由圖可得,步驟2和步驟6查詢結(jié)果集沒有變化,
看起來RR級(jí)別是已經(jīng)解決幻讀問題啦 ~
4.4.2 RR級(jí)別下,一個(gè)當(dāng)前讀的例子假設(shè)現(xiàn)在有個(gè)
account表
,表中有4條數(shù)據(jù),RR級(jí)別。
開啟事務(wù)A,執(zhí)行當(dāng)前讀 ,查詢id>2的所有記錄。 再開啟事務(wù)B,插入id=5的一條數(shù)據(jù)。 流程如下:
顯然,事務(wù)B執(zhí)行插入操作時(shí),阻塞了~因?yàn)槭聞?wù)A在執(zhí)行
select ... lock in share mode
(當(dāng)前讀)的時(shí)候,不僅在id = 3,4 這2條記錄上加了鎖,而且在
id > 2
這個(gè)范圍上也加了
間隙鎖 。因此,我們可以發(fā)現(xiàn),RR隔離級(jí)別下,加鎖的select, update, delete等語句,會(huì)使用間隙鎖 臨鍵鎖,鎖住索引記錄之間的范圍,避免范圍間插入記錄,以
避免產(chǎn)生幻影行記錄 ,那就是說RR隔離級(jí)別解決了幻讀問題?
4.4.3 這種特殊場景,似乎有幻讀問題其實(shí),上圖事務(wù)A中,多加了
update account set balance=200 where id=5;
這步操作,同一個(gè)事務(wù),相同的sql,查出的結(jié)果集不同了,這個(gè)結(jié)果,就符合了幻讀的定義~這個(gè)問題,親愛的朋友,你覺得它算幻讀問題嗎,所以RR隔離級(jí)別,還是存在幻讀問題吧?歡迎大家評(píng)論區(qū)留言哈。
參考資料 [1] 數(shù)據(jù)庫基礎(chǔ)(四)Innodb MVCC實(shí)現(xiàn)原理: https://zhuanlan.zhihu.com/p/52977862