SQL必知必會(MVCC)


我們知道事務(wù)有 4 個(gè)隔離級別方库,以及可能存在的三種異常問題,如下圖所示:

在 MySQL 中障斋,默認(rèn)的隔離級別是可重復(fù)讀纵潦,可以解決臟讀和不可重復(fù)讀的問題,但不能解決幻讀問題垃环。如果我們想要解決幻讀問題邀层,就需要采用串行化的方式,也就是將隔離級別提升到最高遂庄,但這樣一來就會大幅降低數(shù)據(jù)庫的事務(wù)并發(fā)能力寥院。

有沒有一種方式,可以不采用鎖機(jī)制涧团,而是通過樂觀鎖的方式來解決不可重復(fù)讀和幻讀問題呢只磷?實(shí)際上 MVCC 機(jī)制的設(shè)計(jì),就是用來解決這個(gè)問題的泌绣,它可以在大多數(shù)情況下替代行級鎖钮追,降低系統(tǒng)的開銷。

MVCC 是什么阿迈,解決了什么問題

MVCC 的英文全稱是 Multiversion Concurrency Control元媚,中文翻譯過來就是多版本并發(fā)控制技術(shù)。從名字中也能看出來,MVCC 是通過數(shù)據(jù)行的多個(gè)版本管理來實(shí)現(xiàn)數(shù)據(jù)庫的并發(fā)控制刊棕,簡單來說它的思想就是保存數(shù)據(jù)的歷史版本炭晒。這樣我們就可以通過比較版本號決定數(shù)據(jù)是否顯示出來(具體的規(guī)則后面會介紹到),讀取數(shù)據(jù)的時(shí)候不需要加鎖也可以保證事務(wù)的隔離效果甥角。

通過 MVCC 我們可以解決以下幾個(gè)問題:

    1. 讀寫之間阻塞的問題网严,通過 MVCC 可以讓讀寫互相不阻塞,即讀不阻塞寫嗤无,寫不阻塞讀震束,這樣就可以提升事務(wù)并發(fā)處理能力。
    1. 降低了死鎖的概率当犯。這是因?yàn)?MVCC 采用了樂觀鎖的方式垢村,讀取數(shù)據(jù)時(shí)并不需要加鎖,對于寫操作嚎卫,也只鎖定必要的行嘉栓。
    1. 解決一致性讀的問題。一致性讀也被稱為快照讀拓诸,當(dāng)我們查詢數(shù)據(jù)庫在某個(gè)時(shí)間點(diǎn)的快照時(shí)侵佃,只能看到這個(gè)時(shí)間點(diǎn)之前事務(wù)提交更新的結(jié)果,而不能看到這個(gè)時(shí)間點(diǎn)之后事務(wù)提交的更新結(jié)果奠支。

什么是快照讀趣钱,什么是當(dāng)前讀

那么什么是快照讀呢?快照讀讀取的是快照數(shù)據(jù)胚宦。不加鎖的簡單的 SELECT 都屬于快照讀,比如這樣:

SELECT * FROM player WHERE ...

當(dāng)前讀就是讀取最新數(shù)據(jù)燕垃,而不是歷史版本的數(shù)據(jù)枢劝。加鎖的 SELECT,或者對數(shù)據(jù)進(jìn)行增刪改都會進(jìn)行當(dāng)前讀卜壕,比如:

SELECT * FROM player LOCK IN SHARE MODE;
SELECT * FROM player FOR UPDATE;
INSERT INTO player values ...
DELETE FROM player WHERE ...
UPDATE player SET ...

這里需要說明的是您旁,快照讀就是普通的讀操作,而當(dāng)前讀包括了加鎖的讀取和 DML 操作轴捎。

上面講 MVCC 的作用鹤盒,你可能覺得有些抽象。我們用具體的例子體會一下侦副。

比如我們有個(gè)賬戶金額表 user_balance侦锯,包括三個(gè)字段,分別是 username 用戶名秦驯、balance 余額和 bankcard 卡號尺碰,具體的數(shù)據(jù)示意如下:

為了方便,我們假設(shè) user_balance 表中只有用戶 A 和 B 有余額,其他人的賬戶余額均為 0亲桥。下面我們考慮一個(gè)使用場景洛心。

用戶 A 和用戶 B 之間進(jìn)行轉(zhuǎn)賬,此時(shí)數(shù)據(jù)庫管理員想要查詢 user_balance 表中的總金額:

SELECT SUM(balance) FROM user_balance

你可以思考下题篷,如果數(shù)據(jù)庫不支持 MVCC 機(jī)制词身,而是采用自身的鎖機(jī)制來實(shí)現(xiàn)的話,可能會出現(xiàn)怎樣的情況呢番枚?

情況 1:因?yàn)樾枰捎眉有墟i的方式法严,用戶 A 給 B 轉(zhuǎn)賬時(shí)間等待很久,如下圖所示户辫。

你能看到為了保證數(shù)據(jù)的一致性渐夸,我們需要給統(tǒng)計(jì)到的數(shù)據(jù)行都加上行鎖。這時(shí)如果 A 所在的數(shù)據(jù)行加上了行鎖渔欢,就不能給 B 轉(zhuǎn)賬了墓塌,只能等到所有操作完成之后,釋放行鎖再繼續(xù)進(jìn)行轉(zhuǎn)賬奥额,這樣就會造成用戶事務(wù)處理的等待時(shí)間過長苫幢。

情況 2:當(dāng)我們讀取的時(shí)候用了加行鎖,可能會出現(xiàn)死鎖的情況垫挨,如下圖所示韩肝。比如當(dāng)我們讀到 A 有 1000 元的時(shí)候,此時(shí) B 開始執(zhí)行給 A 轉(zhuǎn)賬:

UPDATE user_balance SET balance=balance-100 WHERE username ='B' 

執(zhí)行完之后馬上執(zhí)行下一步:

UPDATE user_balance SET balance=balance+100 WHERE username ='A'

我們會發(fā)現(xiàn)此時(shí) A 被鎖住了九榔,而管理員事務(wù)還需要對 B 進(jìn)行訪問哀峻,但 B 被用戶事務(wù)鎖住了,此時(shí)就發(fā)生了死鎖哲泊。

MVCC 可以解決讀寫互相阻塞的問題剩蟀,這樣提升了效率,同時(shí)因?yàn)椴捎昧藰酚^鎖的思想切威,降低了死鎖的概率育特。

InnoDB 中的 MVCC 是如何實(shí)現(xiàn)的?

我剛才講解了 MVCC 的思想和作用先朦,實(shí)際上 MVCC 沒有正式的標(biāo)準(zhǔn)缰冤,所以在不同的 DBMS 中,MVCC 的實(shí)現(xiàn)方式可能是不同的喳魏,你可以參考相關(guān)的 DBMS 文檔棉浸。今天我來講一下 InnoDB 中 MVCC 的實(shí)現(xiàn)機(jī)制。

在了解 InnoDB 中 MVCC 的實(shí)現(xiàn)方式之前截酷,我們需要了解 InnoDB 是如何存儲記錄的多個(gè)版本的涮拗。這里的多版本對應(yīng)的就是 MVCC 前兩個(gè)字母的釋義:Multi Version乾戏,我們需要了解和它相關(guān)的數(shù)據(jù)都有哪些,存儲在哪里三热。這些數(shù)據(jù)包括事務(wù)版本號鼓择、行記錄中的隱藏列和 Undo Log。

事務(wù)版本號

每開啟一個(gè)事務(wù)就漾,我們都會從數(shù)據(jù)庫中獲得一個(gè)事務(wù) ID(也就是事務(wù)版本號)呐能,這個(gè)事務(wù) ID 是自增長的,通過 ID 大小抑堡,我們就可以判斷事務(wù)的時(shí)間順序摆出。

行記錄的隱藏列

InnoDB 的葉子段存儲了數(shù)據(jù)頁,數(shù)據(jù)頁中保存了行記錄首妖,而在行記錄中有一些重要的隱藏字段偎漫,如下圖所示:

    1. db_row_id:隱藏的行 ID,用來生成默認(rèn)聚集索引有缆。如果我們創(chuàng)建數(shù)據(jù)表的時(shí)候沒有指定聚集索引象踊,這時(shí) InnoDB 就會用這個(gè)隱藏 ID 來創(chuàng)建聚集索引。采用聚集索引的方式可以提升數(shù)據(jù)的查找效率棚壁。
    1. db_trx_id:操作這個(gè)數(shù)據(jù)的事務(wù) ID杯矩,也就是最后一個(gè)對該數(shù)據(jù)進(jìn)行插入或更新的事務(wù) ID。
    1. db_roll_ptr:回滾指針袖外,也就是指向這個(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鬓照。這樣如果我們想要找歷史快照魄幕,就可以通過遍歷回滾指針的方式進(jìn)行查找。

Read View 是如何工作的

在 MVCC 機(jī)制中颖杏,多個(gè)事務(wù)對同一個(gè)行記錄進(jìn)行更新會產(chǎn)生多個(gè)歷史快照,這些歷史快照保存在 Undo Log 里坛芽。如果一個(gè)事務(wù)想要查詢這個(gè)行記錄留储,需要讀取哪個(gè)版本的行記錄呢?這時(shí)就需要用到 Read View 了咙轩,它幫我們解決了行的可見性問題获讳。Read View 保存了當(dāng)前事務(wù)開啟時(shí)所有活躍(還沒有提交)的事務(wù)列表,換個(gè)角度你可以理解為 Read View 保存了不應(yīng)該讓這個(gè)事務(wù)看到的其他的事務(wù) ID 列表活喊。

在 Read VIew 中有幾個(gè)重要的屬性:

    1. trx_ids丐膝,系統(tǒng)當(dāng)前正在活躍的事務(wù) ID 集合。
    1. low_limit_id,活躍的事務(wù)中最大的事務(wù) ID帅矗。
    1. up_limit_id偎肃,活躍的事務(wù)中最小的事務(wù) ID。
    1. creator_trx_id浑此,創(chuàng)建這個(gè) Read View 的事務(wù) ID累颂。

如圖所示,trx_ids 為 trx2凛俱、trx3紊馏、trx5 和 trx8 的集合,活躍的最大事務(wù) ID(low_limit_id)為 trx8蒲犬,活躍的最小事務(wù) ID(up_limit_id)為 trx2朱监。

假設(shè)當(dāng)前有事務(wù) creator_trx_id 想要讀取某個(gè)行記錄,這個(gè)行記錄的事務(wù) ID 為 trx_id原叮,那么會出現(xiàn)以下幾種情況赫编。

如果 trx_id < 活躍的最小事務(wù) ID(up_limit_id),也就是說這個(gè)行記錄在這些活躍的事務(wù)創(chuàng)建之前就已經(jīng)提交了篇裁,那么這個(gè)行記錄對該事務(wù)是可見的沛慢。

如果 trx_id > 活躍的最大事務(wù) ID(low_limit_id),這說明該行記錄在這些活躍的事務(wù)創(chuàng)建之后才創(chuàng)建达布,那么這個(gè)行記錄對當(dāng)前事務(wù)不可見团甲。

如果 up_limit_id < trx_id < low_limit_id,說明該行記錄所在的事務(wù) trx_id 在目前 creator_trx_id 這個(gè)事務(wù)創(chuàng)建的時(shí)候黍聂,可能還處于活躍的狀態(tài)躺苦,因此我們需要在 trx_ids 集合中進(jìn)行遍歷,如果 trx_id 存在于 trx_ids 集合中产还,證明這個(gè)事務(wù) trx_id 還處于活躍狀態(tài)匹厘,不可見。否則脐区,如果 trx_id 不存在于 trx_ids 集合中愈诚,證明事務(wù) trx_id 已經(jīng)提交了,該行記錄可見牛隅。

了解了這些概念之后炕柔,我們來看下當(dāng)查詢一條記錄的時(shí)候,系統(tǒng)如何通過多版本并發(fā)控制技術(shù)找到它:

    1. 首先獲取事務(wù)自己的版本號媒佣,也就是事務(wù) ID匕累;
    1. 獲取 Read View;
    1. 查詢得到的數(shù)據(jù)默伍,然后與 Read View 中的事務(wù)版本號進(jìn)行比較欢嘿;
    1. 如果不符合 ReadView 規(guī)則衰琐,就需要從 Undo Log 中獲取歷史快照;
    1. 最后返回符合規(guī)則的數(shù)據(jù)炼蹦。

你能看到 InnoDB 中羡宙,MVCC 是通過 Undo Log + Read View 進(jìn)行數(shù)據(jù)讀取,Undo Log 保存了歷史快照框弛,而 Read View 規(guī)則幫我們判斷當(dāng)前版本的數(shù)據(jù)是否可見辛辨。

需要說明的是,在隔離級別為讀已提交(Read Commit)時(shí)瑟枫,一個(gè)事務(wù)中的每一次 SELECT 查詢都會獲取一次 Read View斗搞。如表所示:

你能看到,在讀已提交的隔離級別下慷妙,同樣的查詢語句都會重新獲取一次 Read View僻焚,這時(shí)如果 Read View 不同,就可能產(chǎn)生不可重復(fù)讀或者幻讀的情況膝擂。

當(dāng)隔離級別為可重復(fù)讀的時(shí)候虑啤,就避免了不可重復(fù)讀,這是因?yàn)橐粋€(gè)事務(wù)只在第一次 SELECT 的時(shí)候會獲取一次 Read View架馋,而后面所有的 SELECT 都會復(fù)用這個(gè) Read View狞山,如下表所示:

InnoDB 是如何解決幻讀的

不過這里需要說明的是,在可重復(fù)讀的情況下叉寂,InnoDB 可以通過 Next-Key 鎖 +MVCC 來解決幻讀問題萍启。

在讀已提交的情況下,即使采用了 MVCC 方式也會出現(xiàn)幻讀屏鳍。如果我們同時(shí)開啟事務(wù) A 和事務(wù) B勘纯,先在事務(wù) A 中進(jìn)行某個(gè)條件范圍的查詢,讀取的時(shí)候采用排它鎖钓瞭,在事務(wù) B 中增加一條符合該條件范圍的數(shù)據(jù)驳遵,并進(jìn)行提交,然后我們在事務(wù) A 中再次查詢該條件范圍的數(shù)據(jù)山涡,就會發(fā)現(xiàn)結(jié)果集中多出一個(gè)符合條件的數(shù)據(jù)堤结,這樣就出現(xiàn)了幻讀。

出現(xiàn)幻讀的原因是在讀已提交的情況下鸭丛,InnoDB 只采用記錄鎖(Record Locking)霍殴。這里要介紹下 InnoDB 三種行鎖的方式:

    1. 記錄鎖:針對單個(gè)行記錄添加鎖。
    1. 間隙鎖(Gap Locking):可以幫我們鎖住一個(gè)范圍(索引之間的空隙)系吩,但不包括記錄本身。采用間隙鎖的方式可以防止幻讀情況的產(chǎn)生妒蔚。
    1. Next-Key 鎖:幫我們鎖住一個(gè)范圍穿挨,同時(shí)鎖定記錄本身月弛,相當(dāng)于間隙鎖 + 記錄鎖,可以解決幻讀的問題科盛。

在隔離級別為可重復(fù)讀時(shí)帽衙,InnoDB 會采用 Next-Key 鎖的機(jī)制,幫我們解決幻讀問題贞绵。

還是這個(gè)例子厉萝,我們能看到當(dāng)我們想要插入球員艾利克斯·倫(身高 2.16 米)的時(shí)候,事務(wù) B 會超時(shí)榨崩,無法插入該數(shù)據(jù)谴垫。這是因?yàn)椴捎昧?Next-Key 鎖,會將 height>2.08 的范圍都進(jìn)行鎖定母蛛,就無法插入符合這個(gè)范圍的數(shù)據(jù)了翩剪。然后事務(wù) A 重新進(jìn)行條件范圍的查詢,就不會出現(xiàn)幻讀的情況彩郊。

總結(jié)

今天關(guān)于 MVCC 的內(nèi)容有些多前弯,通過學(xué)習(xí)你應(yīng)該能對采用 MVCC 這種樂觀鎖的方式來保證事務(wù)的隔離效果更有體會。

我們需要記住秫逝,MVCC 的核心就是 Undo Log+ Read View恕出,“MV”就是通過 Undo Log 來保存數(shù)據(jù)的歷史版本,實(shí)現(xiàn)多版本的管理违帆,“CC”是通過 Read View 來實(shí)現(xiàn)管理浙巫,通過 Read View 原則來決定數(shù)據(jù)是否顯示。同時(shí)針對不同的隔離級別前方,Read View 的生成策略不同狈醉,也就實(shí)現(xiàn)了不同的隔離級別。

MVCC 是一種機(jī)制惠险,MySQL苗傅、Oracle、SQL Server 和 PostgreSQL 的實(shí)現(xiàn)方式均有不同班巩,我們在學(xué)習(xí)的時(shí)候渣慕,更主要的是要理解 MVCC 的設(shè)計(jì)思想。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末抱慌,一起剝皮案震驚了整個(gè)濱河市逊桦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抑进,老刑警劉巖强经,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寺渗,居然都是意外死亡匿情,警方通過查閱死者的電腦和手機(jī)兰迫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來炬称,“玉大人汁果,你說我怎么就攤上這事×崆” “怎么了据德?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長跷车。 經(jīng)常有香客問我棘利,道長,這世上最難降的妖魔是什么姓赤? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任赡译,我火速辦了婚禮,結(jié)果婚禮上不铆,老公的妹妹穿的比我還像新娘蝌焚。我一直安慰自己,他們只是感情好誓斥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布只洒。 她就那樣靜靜地躺著,像睡著了一般劳坑。 火紅的嫁衣襯著肌膚如雪毕谴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天距芬,我揣著相機(jī)與錄音涝开,去河邊找鬼。 笑死框仔,一個(gè)胖子當(dāng)著我的面吹牛舀武,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播离斩,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼银舱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了跛梗?” 一聲冷哼從身側(cè)響起寻馏,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎核偿,沒想到半個(gè)月后诚欠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年轰绵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了家乘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡藏澳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耀找,到底是詐尸還是另有隱情翔悠,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布野芒,位于F島的核電站蓄愁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏狞悲。R本人自食惡果不足惜撮抓,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望摇锋。 院中可真熱鬧丹拯,春花似錦、人聲如沸荸恕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽融求。三九已至咬像,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間生宛,已是汗流浹背县昂。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陷舅,地道東北人倒彰。 一個(gè)月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像蔑赘,于是被迫代替她去往敵國和親狸驳。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容