MVCC基本實現(xiàn)原理以及與事務(wù)隔離級別的關(guān)聯(lián)

1. 基礎(chǔ)知識

1.1 常規(guī)讀和帶鎖讀

  1. 帶鎖讀(當(dāng)前讀):如select .. lock in share modeselect .. for update甜橱、以及隱含當(dāng)前讀的insertupdate略板、delete等(讀出來才能進(jìn)行更新/刪除/唯一索引判斷等)
  2. 常規(guī)讀(一致性讀):如常用的select ...

【帶鎖讀】通過加鎖的方式保證事務(wù)隔離特性(有無臟讀/不可重復(fù)讀/幻讀等)洼哎;
【常規(guī)讀】則是通過 多版本并發(fā)控制機制(MVCC,Multi-Version Concurrency Control)實現(xiàn)让簿。

插入/更新/刪除等寫操作時:既會加鎖保證【帶鎖讀】的隔離特性;也會備份之前版本的數(shù)據(jù)用于MVCC秀睛,以保證【常規(guī)讀】的隔離特性(詳見本文第二節(jié))尔当。

1.2 事務(wù)隔離級別

隔離級別 臟讀
(Dirty Read)
不可重復(fù)讀
(Non-Repeatable Read)
幻讀
(Phantom Read)
未提交讀
(UNCOMMITTED)
? ? ?
提交讀
(READ COMMITTED)
- ? ?
可重復(fù)讀
(REPEATABLE READ)
- - -
串行化
(SERIALIZABLE)
- - -

值得注意的是:MySQL InnoDB中默認(rèn)的隔離級別【可重復(fù)讀】下,是不存在幻讀問題(帶鎖讀/常規(guī)讀下的幻讀)蹂安。


本事務(wù)讀到其他事務(wù)尚未提交的數(shù)據(jù)時椭迎,稱之為【臟讀】。
這里的【臟】指的是【未提交的數(shù)據(jù)】田盈,這個和讀到【過期的數(shù)據(jù)】是不同的畜号。

比如某個時刻,a=1 已經(jīng)被其他事務(wù)更新成 a=2 且提交了缠黍,而我這個事務(wù)還是讀到a=1弄兜,這就是讀到過期數(shù)據(jù)了,可以稱之為【過期讀】瓷式。
而如果其他事務(wù)更新 a=2 尚未提交,我這個事務(wù)就讀到了a=2语泽,這個就是【臟讀】了贸典。

臟讀通常是不可容忍的,除非有特殊要求踱卵,否則隔離級別一般不會設(shè)置為【未提交讀】廊驼。


同一個事務(wù)中,同樣的SQL惋砂,多次查詢妒挎,查詢結(jié)果不一樣時,稱之為【不可重復(fù)讀】西饵。
例如 同一個事務(wù)中酝掩,第一次查詢 [name=zhangsan] 但是第二次查就變?yōu)榱耍篬name=lisi]

相反地,如果同一個事務(wù)中眷柔,每次查詢結(jié)果都不會變時期虾,自然就是【可重復(fù)讀】了原朝。
【可重復(fù)讀】隔離級別下,讀到的數(shù)據(jù)有可能是過期的镶苞,但不會是臟讀喳坠。

類似select * from t where id=1select * from t where id=1 for update并不屬于同樣的SQL。所以哪怕是在【可重復(fù)讀】的隔離級別下茂蚓,同一個事務(wù)中壕鹉,這兩條SQL查詢結(jié)果不一樣也是正常的。


同一個事務(wù)中聋涨,同樣的SQL晾浴,多次查詢,查詢的結(jié)果集不一樣時牛郑,稱之為【幻讀
例如 同一個事務(wù)中怠肋,第一次查詢結(jié)果為一行,但是第二次查詢就變成兩行了淹朋。

【不可重復(fù)讀】關(guān)注的是某行內(nèi)容是否發(fā)生變化笙各,而【幻讀】則關(guān)注行數(shù)量是否發(fā)生變化。

注:本文幻讀的含義主要參考MySQL官網(wǎng)文檔:14.7.4 Phantom Rows 础芍;即快照讀和當(dāng)前讀 認(rèn)為是不同的 query.

2. MVCC

2.1 多個版本的行數(shù)據(jù)

InnoDB中記錄數(shù)據(jù)的基本單位為頁(InnoDB Page杈抢,默認(rèn)16KB),頁的類型有有多種的仑性,比如存儲當(dāng)前數(shù)據(jù)的數(shù)據(jù)頁(B-Tree Node)惶楼、存儲邏輯回滾/備份數(shù)據(jù)的undo 頁(Undo Log Page)等。

當(dāng)執(zhí)行insert/update/delete寫操作時诊杆,除了要修改對應(yīng)數(shù)據(jù)頁之外歼捐,還會對之前的數(shù)據(jù)進(jìn)行備份(記錄至undo頁中)。如果事務(wù)需要回滾晨汹,找到對應(yīng)的undo 記錄進(jìn)行應(yīng)用回滾即可豹储。
注意:哪怕事務(wù)尚未提交,寫操作也會立即修改當(dāng)前的數(shù)據(jù)頁淘这。所以回滾要到undo log中找剥扣。

顯然,行數(shù)據(jù)是會有多個版本的(當(dāng)前數(shù)據(jù)頁 + undo頁)铝穷,為了區(qū)分各個版本的數(shù)據(jù)钠怯,每一行記錄都會額外多出一個隱藏的版本號字段(trx_id),trx_id即對應(yīng)寫操作的事務(wù)id曙聂。

每個事務(wù)都能分配到一個全局遞增的事務(wù)id(trx_id)晦炊,當(dāng)該事務(wù)進(jìn)行寫操作時,會將該值一并寫入行記錄中(見下例)。


例一:當(dāng)前事務(wù)id=10刽锤,插入:[id=1, name=zhangsan]

  1. 找到可以插入的數(shù)據(jù)頁镊尺;
  2. 寫入記錄:[id=1, name=zhangsan, trx_id=10]
    同時生成undo log:[log_type="insert", id=1]

*回滾*時:找到undo log進(jìn)行應(yīng)用并思,刪除id=1的記錄(插入的反操作)庐氮。

例二:當(dāng)前事務(wù)id=20,更新:[set name=lisi where id = 1]

  1. 找到對應(yīng)記錄的所在記錄頁宋彼;
  2. 修改記錄為:[id=1, name=lisi, trx_id=20]
    同時生成undo log:[log_type="update", id=1, name=zhangsan, trx_id=10]

*回滾*時:找到undo log進(jìn)行應(yīng)用弄砍,反向更新數(shù)據(jù)回 [id=1, name=zhangsan, trx_id=10]

注:兩條undo log記錄可能不在同一個undo頁中

例三:當(dāng)前事務(wù)id=30,刪除:[id=1]

  1. 找到對應(yīng)記錄的所在記錄頁输涕;
  2. 修改記錄為:[id=1, name=lisi, trx_id=30, delete_flag=1]音婶,
    同時生成undo log:[log_type="update", id=1, name=lisi, trx_id=20, delete_flag=0]

執(zhí)行刪除SQL時,并不是直接將記錄從數(shù)據(jù)頁中抹掉莱坎,而是通過一個刪除位(delete_flag)來進(jìn)行標(biāo)識衣式,將該字段置為1即標(biāo)識這行數(shù)據(jù)已經(jīng)被刪除了;同時和其他寫一樣會記錄操作事務(wù)的trx_id檐什。

*回滾*時:找到undo log進(jìn)行應(yīng)用碴卧,反向更新數(shù)據(jù)回 [id=1, name=lisi, trx_id=20, delete_flag=0]

undo log的具體記錄字段可以稍微了解下:

  1. insert into... :含主鍵;
  2. delete .. :含所有字段的之前的值乃正。
  3. update .. :含需要更新字段的之前的值住册;
    如果是更新主鍵,等同于將之前行記錄的刪除瓮具,然后再插入荧飞,將產(chǎn)生兩條undo log。

undo log除了用于備份數(shù)據(jù)支持事務(wù)回滾之外名党,其數(shù)據(jù)多版本的特性與事務(wù)快照結(jié)合之后叹阔,將可以用于支持事務(wù)隔離的相關(guān)特性(比如避免臟讀/不可重復(fù)讀/幻讀等)。

2.2 事務(wù)快照

大家應(yīng)該拍過照片传睹,按下快門条获,我們就可以將當(dāng)前時刻的景物記錄到一張小小的照片,盡管時光荏苒蒋歌,歲月變遷,照片中的景物也不會發(fā)生變化委煤。

如果我們給數(shù)據(jù)庫中的事務(wù)拍一張照片的話堂油,我們會看到:在拍照的那一瞬間,有的事務(wù)已經(jīng)提交碧绞,有的正在運行中府框,有的事務(wù)尚未開始

事務(wù)快照,黑色表示事務(wù)已提交

就如上圖中的快照讥邻,trx_id小于15的事務(wù)都已經(jīng)提交了迫靖,大于等于31的則尚未開始院峡;中間的15/25還在跑,而20/30已經(jīng)提交系宜。

如果你現(xiàn)在的事務(wù)id為25照激,當(dāng)隔離級別為【可重復(fù)讀】時:你能到哪些事務(wù)修改的數(shù)據(jù)呢?答案是顯然的盹牧,已經(jīng)提交的則看得見(圖中黑色)俩垃,還沒提交的自然就看不見,否則就是臟讀了汰寓。

可時間是會變化的口柳,假設(shè)后來15進(jìn)行了提交,那我們能否看得見該事務(wù)的修改記錄呢(比如 a=1 修改為了 a=2)有滑?這個也是應(yīng)該看不見的跃闹,因為如果事務(wù)15提交前我們看到的是a=1,而提交后變?yōu)閍=2了毛好,這就出現(xiàn)了不可重復(fù)讀了望艺,這顯然和【可重復(fù)讀】相悖了。
事實上睛榄,正如前面所說時間會變但照片不變一樣荣茫,一旦我們拍下事務(wù)快照之后,id=15的事務(wù)對于咱們來講,“它一直都是處于未提交的”(除非我們重新拍過另外一張快照)麦备。

在【可重復(fù)讀】隔離級別下腹缩,一旦觸發(fā)快照后,這個快照會一直存在咧欣,直至事務(wù)結(jié)束。哪些事務(wù)已提交轨帜,哪些沒提交魄咕,也會在這一瞬間定格。這也就保證了我們永遠(yuǎn)都在同一張照片里面“找”數(shù)據(jù)蚌父,從而保證了【可重復(fù)讀】哮兰。
接下來我們來看一下怎樣基于事務(wù)快照來“找”數(shù)據(jù)。

注:【可重復(fù)讀】隔離級別下苟弛,事務(wù)快照的觸發(fā)時機主要有:

  • 開啟事務(wù)后(begin/start transaction;)喝滞,執(zhí)行第一條常規(guī)讀SQL(select)時;
  • 開啟事務(wù)時膏秫,直接開啟快照:start transaction with consistent snapshot.

2.3 MVCC查詢基本流程

基于數(shù)據(jù)快照和多版本數(shù)據(jù)右遭,查詢的大概過程為:

  1. 觸發(fā)事務(wù)快照
  2. 根據(jù)查詢條件找到的數(shù)據(jù)頁中的記錄,獲取該數(shù)據(jù)的版本號(即寫入該記錄的事務(wù)trx_id
  3. 基于快照窘哈,判斷這個寫入記錄的事務(wù)(trx_id)對于快照來講是否可見
    3.1 如果可見吹榴,則返回結(jié)果;
    3.2 如果不可見滚婉,繼續(xù)找下一個版本的數(shù)據(jù)图筹。

我們可以用一個簡單的數(shù)據(jù)結(jié)構(gòu)(Read View)來記錄事務(wù)快照(建議結(jié)合上節(jié)的事務(wù)快照圖看):

Read_View {
  // 最小的事務(wù)id,數(shù)據(jù)版本號 < min_id 表示可見
  long min_id;      

  // 最大的事務(wù)id满哪,數(shù)據(jù)版本號 >= max_id 表示不可見
  long max_id;      

  // 中間還在跑的事務(wù)id婿斥,數(shù)據(jù)版本號在里面則表示不可見(排除本事務(wù),自己肯定看得到自己修改的記錄)
  long[] running_ids;   

  // 是否可見
  bool canSee(long data_version_trx_id) {
    return data_version_trx_id < min_id || !running_ids.contains(data_version_trx_id);
  }
}

假設(shè)時間上有那么三個寫操作哨鸭,

  1. 插入記錄:[id=1, name=zhangsan] 民宿,操作的事務(wù)trx_id = 10
  2. 更新記錄為:[id=1, name=lisi],操作的事務(wù)trx_id = 20
  3. 刪除該記錄:操作的事務(wù)trx_id = 30

都執(zhí)行后像鸡,其數(shù)據(jù)多版本的一個呈現(xiàn)如下圖:

如果期間有其他事務(wù)有觸發(fā)過快照活鹰,基于【可重復(fù)讀】的隔離級別,快照之后讀到的數(shù)據(jù)都是一樣的(同一個事務(wù)中)只估。我們來分析一下志群,等上面三個操作均執(zhí)行完成之后,我們是怎么追溯回快照時刻的數(shù)據(jù)的蛔钙。


例一:假設(shè)本事務(wù)在某個時刻建立了快照:[min_trx_id=40, max_trx_id=50, running_ids=[40]]锌云,而后在某個時刻發(fā)起查詢select * from t where id=1

快照時刻,事務(wù)10/20/30均已經(jīng)提交了吁脱,所以最新的修改記錄就是事務(wù)30將這條記錄給刪了桑涎,這個“刪除”的修改對于快照是可見的,所以結(jié)果返回空了兼贡。


例二:假設(shè)本事務(wù)在某個時刻建立了快照:[min_trx_id=15, max_trx_id=28, running_ids=[15]]攻冷,而后在某個時刻發(fā)起查詢select * from t where id=1

快照時刻,事務(wù)10/20已經(jīng)提交遍希,而事務(wù)30尚未開始等曼,所以能看到所有已經(jīng)提交中最新的記錄,即事務(wù)20:更新記錄為[id=1, name=lisi]


例三:假設(shè)本事務(wù)在某個時刻建立了快照:[min_trx_id=10, max_trx_id=28, running_ids=[10, 20]]凿蒜,而后在某個時刻發(fā)起查詢select * from t where id=1;

快照時刻禁谦,事務(wù)30尚未開始,事務(wù)10/20均在運行中废封,均屬于未提交枷畏;插入的事務(wù)(10)都尚未提交,所以都看不見虱饿,最終返回空。

關(guān)于undo log的清除:

  1. 對于運行中事務(wù)引用到的undo log,不可以清除氮发,因為可能要用于回滾渴肉;
  2. 對于插入產(chǎn)生的undo log,在對應(yīng)寫事務(wù)結(jié)束后便可以刪除了爽冕;因為對于"insert"類型的undo對于其他事務(wù)來講等同于空(插入之前的數(shù)據(jù)自然是空的)仇祭。
  3. 對于其他類型的undo log,將對被定期清除(Purge)颈畸,前提是要確定當(dāng)前所有的事務(wù)快照不會再有機會用到(到達(dá))該版本的數(shù)據(jù)了乌奇。

可以看到,事務(wù)快照不變時眯娱,看到的數(shù)據(jù)將始終停留在某一個版本的礁苗。

  • 隔離級別為【可重復(fù)讀】時,一旦獲取快照后徙缴,會一直用這個快照试伙,從而保證不會出現(xiàn)【不可重復(fù)讀】∮谘基于快照疏叨,如果插入數(shù)據(jù)的事務(wù)尚未提交,也是不可見的穿剖,這也就避免了【幻讀】蚤蔓。
  • 隔離級別為【提交讀】時,每次常規(guī)讀(select)都會創(chuàng)建一個新的事務(wù)快照糊余,所以每次讀到的都是最新快照時刻的數(shù)據(jù)秀又;這也就導(dǎo)致了【不可重復(fù)讀】。
  • 隔離級別為【未提交讀】時啄刹,并沒有使用快照涮坐,而是無論事務(wù)有無提交,直接讀數(shù)據(jù)頁中的行記錄作為結(jié)果(undo頁的數(shù)據(jù)都不管)誓军,從而導(dǎo)致【臟讀】袱讹。

3. 總結(jié)

  • 帶鎖讀通過加鎖保證事務(wù)特性,而常規(guī)讀通過MVCC實現(xiàn)昵时;
  • 當(dāng)執(zhí)行寫SQL時捷雕,除了寫數(shù)據(jù)頁,還會記錄undo log壹甥;undo log可用于以前版本數(shù)據(jù)的回溯救巷;
  • 同一個事務(wù)快照,常規(guī)讀 讀到的數(shù)據(jù) 一直都是一致的句柠;
  • 針對不同的隔離級別浦译,常規(guī)讀時:
    • 未提交讀:直接讀取當(dāng)前數(shù)據(jù)頁的數(shù)據(jù)(可能臟讀)棒假;
    • 提交讀:每次讀都建立新快照,會讀到已提交的精盅、最新的數(shù)據(jù)(無臟讀帽哑、可能不可重復(fù)讀)
    • 可重復(fù)讀:只會在第一次讀時(或start transaction with consistent snapshot)建立快照,之后的常規(guī)讀均基于該快照(無臟讀叹俏、無不可重復(fù)讀妻枕、無幻讀)
    • 串行化:主動開啟事務(wù)查詢時,會將常規(guī)select將被轉(zhuǎn)換為select .. lock in share mode粘驰,通過加鎖的方式(參考可重復(fù)讀)保證事務(wù)特性(無臟讀屡谐、無不可重復(fù)讀、無幻讀)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蝌数,一起剝皮案震驚了整個濱河市愕掏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌籽前,老刑警劉巖亭珍,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異枝哄,居然都是意外死亡肄梨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門挠锥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來众羡,“玉大人,你說我怎么就攤上這事蓖租×宦拢” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵蓖宦,是天一觀的道長齐婴。 經(jīng)常有香客問我,道長稠茂,這世上最難降的妖魔是什么柠偶? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮睬关,結(jié)果婚禮上诱担,老公的妹妹穿的比我還像新娘。我一直安慰自己电爹,他們只是感情好蔫仙,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丐箩,像睡著了一般摇邦。 火紅的嫁衣襯著肌膚如雪恤煞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天涎嚼,我揣著相機與錄音阱州,去河邊找鬼。 笑死法梯,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的犀概。 我是一名探鬼主播立哑,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼姻灶!你這毒婦竟也來了铛绰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤产喉,失蹤者是張志新(化名)和其女友劉穎捂掰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體曾沈,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡这嚣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了塞俱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姐帚。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖障涯,靈堂內(nèi)的尸體忽然破棺而出罐旗,到底是詐尸還是另有隱情,我是刑警寧澤唯蝶,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布九秀,位于F島的核電站,受9級特大地震影響粘我,放射性物質(zhì)發(fā)生泄漏鼓蜒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一涂滴、第九天 我趴在偏房一處隱蔽的房頂上張望友酱。 院中可真熱鬧,春花似錦柔纵、人聲如沸缔杉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽或详。三九已至系羞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間霸琴,已是汗流浹背椒振。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梧乘,地道東北人澎迎。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像选调,于是被迫代替她去往敵國和親夹供。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

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