MySQL之MVCC機制:為什么你改了的數(shù)據(jù)我還看不見?

一蓖柔、并發(fā)事務(wù)的四種場景

并發(fā)事務(wù)中又會分為四種情況辰企,分別是讀-讀、寫-寫况鸣、讀-寫牢贸、寫-讀,這四種情況分別對應(yīng)并發(fā)事務(wù)執(zhí)行時的四種場景镐捧,為了后續(xù)分析MVCC機制時方便理解潜索,因此先將這幾種情況說明,咱們首先來看看讀-讀場景懂酱。

1.1竹习、讀-讀場景

讀-讀場景即是指多個事務(wù)/線程在一起讀取一個相同的數(shù)據(jù)丐一,比如事務(wù)T1正在讀取ID=88的行記錄寄纵,事務(wù)T2也在讀取這條記錄嫡霞,兩個事務(wù)之間是并發(fā)執(zhí)行的震檩。

廣為人知的一點:MySQL執(zhí)行查詢語句牺勾,絕對不會對引起數(shù)據(jù)的任何變化怔蚌,因此對于這種情況而言叉跛,不需要做任何操作靖避,因為不改變數(shù)據(jù)就不會引起任何并發(fā)問題九默。

1.2震放、寫-寫場景

寫-寫場景也比較簡單,也就是指多個事務(wù)之間一起對同一數(shù)據(jù)進行寫操作驼修,比如事務(wù)T1ID=88的行記錄做修改操作澜搅,事務(wù)T2則對這條數(shù)據(jù)做刪除操作伍俘,事務(wù)T1提交事務(wù)后想查詢看一下,哦豁勉躺,結(jié)果連這條數(shù)據(jù)都不見了癌瘾,這也是所謂的臟寫問題,也被稱為更新覆蓋問題饵溅,對于這個問題在所有數(shù)據(jù)庫妨退、所有隔離級別中都是零容忍的存在,最低的隔離級別也要解決這個問題蜕企。

1.3咬荷、讀-寫、寫-讀場景

讀-寫轻掩、寫-讀實際上從宏觀角度來看幸乒,可以理解成同一種類型的操作,但從微觀角度而言則是兩種不同的情況唇牧,讀-寫是指一個事務(wù)先開始讀罕扎,然后另一個事務(wù)則過來執(zhí)行寫操作,寫-讀則相反丐重,主要是讀腔召、寫發(fā)生的前后順序的區(qū)別。

并發(fā)事務(wù)中同時存在讀扮惦、寫兩類操作時臀蛛,這是最容易出問題的場景,臟讀崖蜜、不可重復(fù)讀浊仆、幻讀都出自于這種場景中,當(dāng)有一個事務(wù)在做寫操作時豫领,讀的事務(wù)中就有可能出現(xiàn)這一系列問題氧卧,因此數(shù)據(jù)庫才會引入各種機制解決。

1.4氏堤、各場景下解決問題的方案

《MySQL鎖機制》中沙绝,對于寫-寫、讀-寫鼠锈、寫-讀這三類場景闪檬,都是利用加鎖的方案確保線程安全,但上面說到過购笆,加鎖會導(dǎo)致部分事務(wù)串行化粗悯,因此效率會下降,而MVCC機制的誕生則解決了這個問題同欠。

先來設(shè)想一個問題:加鎖的目的是什么样傍?防止臟寫横缔、臟讀、不可重復(fù)讀及幻讀這類問題出現(xiàn)衫哥。

對于臟寫問題茎刚,這是寫-寫場景下會出現(xiàn)的,寫-寫場景必須要加鎖才能保障安全撤逢,因此先將該場景排除在外膛锭。再想想:對于讀-寫并存的場景中,臟讀蚊荣、不可重復(fù)讀及幻讀問題都出自該場景中初狰,但實際項目中,出現(xiàn)這些問題的幾率本身就比較小互例,為了防止一些小概念事件奢入,就將所有操縱同一數(shù)據(jù)的并發(fā)讀寫事務(wù)串行化,這似乎有些不講道理呀媳叨,就好比:

為了防止自家保險柜中的3.25元被偷腥光,所以每天從早到晚一直守著保險柜,這合理嗎肩杈?并不合理柴我,畢竟只有千日做賊解寝,那有千日防賊的道理扩然。

因此MySQL就基于讀-寫并存的場景,推出了MVCC機制聋伦,在線程安全問題和加鎖串行化之間做了一定取舍夫偶,讓兩者之間達到了很好的平衡,即防止了臟讀觉增、不可重復(fù)讀及幻讀問題的出現(xiàn)兵拢,又無需對并發(fā)讀-寫事務(wù)加鎖處理。

咋做到的呢逾礁?接下來一起來好好聊一聊大名鼎鼎的MVCC機制说铃。

二、MySQL-MVCC機制綜述

MVCC機制的全稱為Multi-Version Concurrency Control嘹履,即多版本并發(fā)控制技術(shù)腻扇,主要是為了提升數(shù)據(jù)庫并發(fā)性能而設(shè)計的,其中采用更好的方式處理了讀-寫并發(fā)沖突砾嫉,做到即使有讀寫沖突時幼苛,也可以不加鎖解決,從而確保了任何時刻的讀操作都是非阻塞的焕刮。

但與其說是MySQL-MVCC機制舶沿,還不如說是InnoDB-MVCC機制墙杯,因為在MySQL眾多的開源存儲引擎中,幾乎只有InnoDB實現(xiàn)了MVCC機制括荡,類似于MyISAM高镐、Memory等引擎中都未曾實現(xiàn),那其他引擎為何不實現(xiàn)呢一汽?不是不想避消,而是做不到,這跟MVCC機制的實現(xiàn)原理有關(guān)召夹,這點放在后續(xù)詳細講解~

不過為了更好的理解啥叫MVCC多版本并發(fā)控制岩喷,先來看一個日常生活的例子~

2.1、MVCC技術(shù)在日常生活中的體現(xiàn)

不知道各位小伙伴中监憎,是否有人做過論壇這類業(yè)務(wù)的項目纱意,或者類似審核的業(yè)務(wù)需求,以掘金的文章為例鲸阔,此時來思考一個場景:

假設(shè)我發(fā)布了一篇關(guān)于《MySQL事務(wù)機制》的文章偷霉,發(fā)布后挺受歡迎的,因此有不少小伙伴在看褐筛,其中有一位小伙伴比較細心类少,文中存在兩三個錯別字,被這位小伙伴指出來了渔扎,因此我去修正錯別字后重新發(fā)布硫狞。

問題來了,對于文章首次發(fā)布也好晃痴,重新發(fā)布也罷残吩,絕對要等審核通過后才會正式發(fā)布的,那我修正文章后重新發(fā)布倘核,文章又會進入「審核中」這個狀態(tài)泣侮,此時對于其他正在看、準(zhǔn)備看的小伙伴來說紧唱,文章是不是就不見了活尊?畢竟文章還在審核撒,因此對這個業(yè)務(wù)需求又該如何實現(xiàn)呢漏益?多版本蛹锰!

啥意思呢?也就是說遭庶,對于首次發(fā)布后通過審核的文章宁仔,在后續(xù)重新發(fā)布審核時,用戶可以看到更新前的文章峦睡,也就是看到老版本的文章翎苫,當(dāng)更新后的文章審核通過后权埠,再使用新版本的文章代替老版本的文章即可。

這樣就能做到新老版本的兼容煎谍,也能夠確保文章修正時攘蔽,其他正在閱讀的小伙伴不會受影響,而MySQL-MVCC機制的思想也大致相同呐粘。

2.2满俗、MySQL-MVCC多版本并發(fā)控制

MySQL中的多版本并發(fā)控制,也和上面給出的例子類似作岖,畢竟回想一下唆垃,臟讀、不可重復(fù)讀痘儡、幻讀問題都是由于多個事務(wù)并發(fā)讀寫導(dǎo)致的辕万,但這些問題都是基于最新版本的數(shù)據(jù)并發(fā)操作才會出現(xiàn),那如果讀沉删、寫的事務(wù)操作的不是同一個版本呢渐尿?比如寫操作走新版本,讀操作走老版本矾瑰,這樣是不是無論執(zhí)行寫操作的事務(wù)干了啥砖茸,都不會影響讀的事務(wù)?答案是Yes殴穴。

不過要稍微記住凉夯,MySQL中僅在RC讀已提交級別、RR不可重復(fù)讀級別才會使用MVCC機制推正,Why恍涂?

因為如果是RU讀未提交級別宝惰,既然都允許存在臟讀問題植榕、允許一個事務(wù)讀取另一個事務(wù)未提交的數(shù)據(jù),那自然可以直接讀最新版本的數(shù)據(jù)尼夺,因此無需MVCC介入尊残。

同時如若是Serializable串行化級別,因為會將所有的并發(fā)事務(wù)串行化處理淤堵,也就是不論事務(wù)是讀操作寝衫,疑惑是寫操作,都會被排好隊一個個執(zhí)行拐邪,這都不存在所謂的多線程并發(fā)問題了慰毅,自然也無需MVCC介入。

因此要牢記:MVCC機制在MySQL中扎阶,僅有InnoDB引擎支持汹胃,而在該引擎中婶芭,MVCC機制只對RC、RR兩個隔離級別下的事務(wù)生效着饥。當(dāng)然犀农,RC、RR兩個不同的隔離級別中宰掉,MVCC的實現(xiàn)也存在些許差異呵哨,對于這點后續(xù)詳細講解。

三轨奄、MySQL-MVCC機制實現(xiàn)原理剖析

OK~孟害,簡單理解了啥叫MVCC機制后,接著一起來看看InnoDB引擎是如何實現(xiàn)它的挪拟,MVCC機制主要通過隱藏字段纹坐、Undo-log日志、ReadView這三個東西實現(xiàn)的舞丛,因而這三玩意兒也被稱為“MVCC三劍客”耘子!廢話不多說,一起來看看球切。

3.1谷誓、InnoDB表的隱藏字段

通常而言,當(dāng)你基于InnoDB引擎建立一張表后吨凑,MySQL除開會構(gòu)建你顯式聲明的字段外捍歪,通常還會構(gòu)建一些InnoDB引擎的隱藏字段,在InnoDB引擎中主要有DB_ROW_ID鸵钝、DB_Deleted_Bit糙臼、DB_TRX_ID、DB_ROLL_PTR這四個隱藏字段恩商,挨個簡單介紹一下变逃。

3.1.1、隱藏主鍵 - ROW_ID(6Bytes)

在之前介紹《索引原理篇》的時候聊到過一點怠堪,對于InnoDB引擎的表而言揽乱,由于其表數(shù)據(jù)是按照聚簇索引的格式存儲,因此通常都會選擇主鍵作為聚簇索引列粟矿,然后基于主鍵字段構(gòu)建索引樹凰棉,但如若表中未定義主鍵,則會選擇一個具備唯一非空屬性的字段陌粹,作為聚簇索引的字段來構(gòu)建樹撒犀。

當(dāng)兩者都不存在時,InnoDB就會隱式定義一個順序遞增的列ROW_ID來作為聚簇索引列。

因此要牢記一點或舞,如果你選擇的引擎是InnoDB隧膏,就算你的表中未定義主鍵、索引嚷那,其實默認也會存在一個聚簇索引胞枕,只不過這個索引在上層無法使用,僅提供給InnoDB構(gòu)建樹結(jié)構(gòu)存儲表數(shù)據(jù)魏宽。

3.1.2腐泻、刪除標(biāo)識 - Deleted_Bit(1Bytes)

在之前講《SQL執(zhí)行篇-寫SQL執(zhí)行原理時》,咱們只粗略的過了一下大體流程队询,其中并未涉及到一些細節(jié)闡述派桩,在這里稍微提一下:對于一條delete語句而言,當(dāng)執(zhí)行后并不會立馬刪除表的數(shù)據(jù)蚌斩,而是將這條數(shù)據(jù)的Deleted_Bit刪除標(biāo)識改為1/true铆惑,后續(xù)的查詢SQL檢索數(shù)據(jù)時,如果檢索到了這條數(shù)據(jù)送膳,但看到隱藏字段Deleted_Bit=1時员魏,就知道該數(shù)據(jù)已經(jīng)被其他事務(wù)delete了,因此不會將這條數(shù)據(jù)納入結(jié)果集叠聋。

OK~撕阎,但設(shè)計Deleted_Bit這個隱藏字段的好處是什么呢?主要是能夠有利于聚簇索引碌补,比如當(dāng)一個事務(wù)中刪除一條數(shù)據(jù)后虏束,后續(xù)又執(zhí)行了回滾操作,假設(shè)此時是真正的刪除了表數(shù)據(jù)厦章,會發(fā)生什么情況呢镇匀?

  • ①刪除表數(shù)據(jù)時,有可能會破壞索引樹原本的結(jié)構(gòu)袜啃,導(dǎo)致出現(xiàn)葉子節(jié)點合并的情況汗侵。
  • ②事務(wù)回滾時,又需重新插入這條數(shù)據(jù)囊骤,再次插入時又會破壞前面的結(jié)構(gòu)晃择,導(dǎo)致葉子節(jié)點分裂冀值。

綜上所述也物,如果執(zhí)行delete語句就刪除真實的表數(shù)據(jù),由于事務(wù)回滾的問題列疗,就很有可能導(dǎo)致聚簇索引樹發(fā)生兩次結(jié)構(gòu)調(diào)整滑蚯,這其中的開銷可想而知,而且先刪除,再回滾告材,最終樹又變成了原狀坤次,那這兩次樹的結(jié)構(gòu)調(diào)整還是無意義的。

所以斥赋,當(dāng)執(zhí)行delete語句時缰猴,只會改變將隱藏字段中的刪除標(biāo)識改為1/true,如果后續(xù)事務(wù)出現(xiàn)回滾動作疤剑,直接將其標(biāo)識再改回0/false即可滑绒,這樣就避免了索引樹的結(jié)構(gòu)調(diào)整。

但如若事務(wù)刪除數(shù)據(jù)之后提交了事務(wù)呢隘膘?總不能讓這條數(shù)據(jù)一直留在磁盤吧疑故?畢竟如果所有的delete操作都這么干,就會導(dǎo)致磁盤爆滿~弯菊,顯然這樣是不妥的纵势,因此刪除標(biāo)識為1/true的數(shù)據(jù)最終依舊會從磁盤中移除,啥時候移呢管钳?

在之前講《Nginx-緩存清理》時钦铁,曾經(jīng)提到過purger這一系列的參數(shù),通過配置該系列參數(shù)后才漆,Nginx后臺中會創(chuàng)建對應(yīng)的purger線程去自動刪除緩存數(shù)據(jù)育瓜。而MySQL中也不例外,同樣存在purger線程的概念栽烂,為了防止“已刪除”的數(shù)據(jù)占用過多的磁盤空間躏仇,purger線程會自動清理Deleted_Bit=1/true的行數(shù)據(jù)。

當(dāng)然腺办,為了確保清理數(shù)據(jù)時不會影響MVCC的正常工作焰手,purger線程自身也會維護一個ReadView,如果某條數(shù)據(jù)的Deleted_Bit=true怀喉,并且TRX_IDpurge線程的ReadView可見书妻,那么這條數(shù)據(jù)一定是可以被安全清除的(即不會影響MVCC工作)。

對于上述最后一段大家可能會有些許疑惑躬拢,這是因為還未曾介紹ReadView躲履,因此有些不理解可先跳過,后續(xù)理解了ReadView后再回來看會好很多聊闯。

3.1.3工猜、最近更新的事務(wù)ID - TRX_ID(6Bytes)

TRX_ID全稱為transaction_id,翻譯過來也就是事務(wù)ID的意思菱蔬,MySQL對于每一個創(chuàng)建的事務(wù)篷帅,都會為其分配一個事務(wù)ID史侣,事務(wù)ID同樣遵循順序遞增的特性,即后來的事務(wù)ID絕對會比之前的ID要大魏身,比如:

此時事務(wù)T1準(zhǔn)備修改表字段的值惊橱,MySQL會為其分配一個事務(wù)ID=1,當(dāng)事務(wù)T2準(zhǔn)備向表中插入一條數(shù)據(jù)時箭昵,又會為這個事務(wù)分配一個ID=2......

但有一個細節(jié)點需要記姿捌印:MySQL對于所有包含寫入SQL的事務(wù),會為其分配一個順序遞增的事務(wù)ID家制,但如果是一條select查詢語句掉房,則分配的事務(wù)ID=0

不過對于手動開啟的事務(wù)慰丛,MySQL都會為其分配事務(wù)ID卓囚,就算這個手動開啟的事務(wù)中僅有select操作。

表中的隱藏字段TRX_ID诅病,記錄的就是最近一次改動當(dāng)前這條數(shù)據(jù)的事務(wù)ID哪亿,這個字段是實現(xiàn)MVCC機制的核心之一。

3.1.4贤笆、回滾指針 - ROLL_PTR(7Bytes)

ROLL_PTR全稱為rollback_pointer蝇棉,也就是回滾指針的意思,這個也是表中每條數(shù)據(jù)都會存在的一個隱藏字段芥永,當(dāng)一個事務(wù)對一條數(shù)據(jù)做了改動后篡殷,都會將舊版本的數(shù)據(jù)放到Undo-log日志中,而rollback_pointer就是一個地址指針埋涧,指向Undo-log日志中舊版本的數(shù)據(jù)板辽,當(dāng)需要回滾事務(wù)時,就可以通過這個隱藏列棘催,來找到改動之前的舊版本數(shù)據(jù)劲弦,而MVCC機制也利用這點,實現(xiàn)了行數(shù)據(jù)的多版本醇坝。

3.2邑跪、InnoDB引擎的Undo-log日志

在之前《事務(wù)篇》中分析事務(wù)實現(xiàn)原理時,咱們得知了MySQL事務(wù)機制是基于Undo-log實現(xiàn)的呼猪,同時在剛剛在聊回滾指針時画畅,聊到了Undo-log日志中會存儲舊版本的數(shù)據(jù),但要注意:Undo-log中并不僅僅只存儲一條舊版本數(shù)據(jù)宋距,其實在該日志中會有一個版本鏈轴踱,啥意思呢?舉個例子:

SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊貓      | 女       | 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
UPDATE `zz_users` SET user_sex = "男" WHERE user_id = 1;

比如上述這段SQL隸屬于trx_id=1T1事務(wù)乡革,其中對同一條數(shù)據(jù)改動了兩次寇僧,那Undo-log日志中只會存儲一條舊版本數(shù)據(jù)嗎摊腋?NO沸版,答案是兩條舊版本的數(shù)據(jù)嘁傀,如下圖:

從上圖中可明顯看出:不同的舊版本數(shù)據(jù),會以roll_ptr回滾指針作為鏈接點视粮,然后將所有的舊版本數(shù)據(jù)組成一個單向鏈表细办。但要注意一點:最新的舊版本數(shù)據(jù),都會插入到鏈表頭中蕾殴,而不是追加到鏈表尾部笑撞。

細說一下執(zhí)行上述update語句的詳細過程:
①對ID=1這條要修改的行數(shù)據(jù)加上排他鎖。
②將原本的舊數(shù)據(jù)拷貝到Undo-logrollback Segment區(qū)域钓觉。
③對表數(shù)據(jù)上的記錄進行修改茴肥,修改完成后將隱藏字段中的trx_id改為當(dāng)前事務(wù)ID
④將隱藏字段中的roll_ptr指向Undo-log中對應(yīng)的舊數(shù)據(jù)荡灾,并在提交事務(wù)后釋放鎖瓤狐。

為什么Undo-log日志要設(shè)計出版本鏈呢?兩個好處:一方面可以實現(xiàn)事務(wù)點回滾(這點回去參考事務(wù)篇)批幌,另一方面則可以實現(xiàn)MVCC機制(這點后面聊)础锐。

與之前的刪除標(biāo)識類似,一條數(shù)據(jù)被delete后并提交了荧缘,最終會從磁盤移除皆警,而Undo-log中記錄的舊版本數(shù)據(jù),同樣會占用空間截粗,因此在事務(wù)提交后也會移除信姓,移除的工作同樣由purger線程負責(zé),purger線程內(nèi)部也會維護一個ReadView绸罗,它會以此作為判斷依據(jù)财破,來決定何時移除Undo記錄。

3.3从诲、MVCC核心 - ReadView

MVCC在前面聊到過左痢,它翻譯過來就是多版本并發(fā)控制的意思,對于這個名詞中的多版本已經(jīng)通過Undo-log日志實現(xiàn)了系洛,但再思考一個問題:如果T2事務(wù)要查詢一條行數(shù)據(jù)俊性,此時這條行數(shù)據(jù)正在被T1事務(wù)寫,那也就代表著這條數(shù)據(jù)可能存在多個舊版本數(shù)據(jù)描扯,T2事務(wù)在查詢時定页,應(yīng)該讀這條數(shù)據(jù)的哪個版本呢?此時就需要用到ReadView绽诚,用它來做多版本的并發(fā)控制典徊,根據(jù)查詢的時機來選擇一個當(dāng)前事務(wù)可見的舊版本數(shù)據(jù)讀取杭煎。

那究竟什么是ReadView呢?就是一個事務(wù)在嘗試讀取一條數(shù)據(jù)時卒落,MVCC基于當(dāng)前MySQL的運行狀態(tài)生成的快照羡铲,也被稱之為讀視圖,即ReadView儡毕,在這個快照中記錄者當(dāng)前所有活躍事務(wù)的ID(活躍事務(wù)是指還在執(zhí)行的事務(wù)也切,即未結(jié)束(提交/回滾)的事務(wù))。

當(dāng)一個事務(wù)啟動后腰湾,首次執(zhí)行select操作時雷恃,MVCC就會生成一個數(shù)據(jù)庫當(dāng)前的ReadView,通常而言费坊,一個事務(wù)與一個ReadView屬于一對一的關(guān)系(不同隔離級別下也會存在細微差異)倒槐,ReadView一般包含四個核心內(nèi)容:

  • creator_trx_id:代表創(chuàng)建當(dāng)前這個ReadView的事務(wù)ID
  • trx_ids:表示在生成當(dāng)前ReadView時附井,系統(tǒng)內(nèi)活躍的事務(wù)ID列表讨越。
  • up_limit_id:活躍的事務(wù)列表中,最小的事務(wù)ID羡忘。
  • low_limit_id:表示在生成當(dāng)前ReadView時谎痢,系統(tǒng)中要給下一個事務(wù)分配的ID值。

上面四個值很簡單卷雕,值得一提的是low_limit_id节猿,它并不是目前系統(tǒng)中活躍事務(wù)的最大ID,因為之前講到過漫雕,MySQL的事務(wù)ID是按序遞增的滨嘱,因此當(dāng)啟動一個新的事務(wù)時,都會為其分配事務(wù)ID浸间,而這個low_limit_id則是整個MySQL中太雨,要為下一個事務(wù)分配的ID值。

下面上個ReadView的示意圖魁蒜,來好好理解一下它:

假設(shè)目前數(shù)據(jù)庫中共有T1~T5這五個事務(wù)囊扳,T1、T2兜看、T4還在執(zhí)行锥咸,T3已經(jīng)回滾,T5已經(jīng)提交细移,此時當(dāng)有一條查詢語句執(zhí)行時搏予,就會利用MVCC機制生成一個ReadView,由于前面講過弧轧,單純由一條select語句組成的事務(wù)并不會分配事務(wù)ID雪侥,因此默認為0碗殷,所以目前這個快照的信息如下:

{
    "creator_trx_id" : "0",
    "trx_ids" : "[1,2,4]",
    "up_limit_id" : "1",
    "low_limit_id" : "6"
}

OK~,簡單明白ReadView的結(jié)構(gòu)后速缨,接著一起來聊一聊MVCC機制的實現(xiàn)原理锌妻。

3.4、MVCC機制實現(xiàn)原理

將“MVCC三劍客”的概念闡述完畢后鸟廓,再結(jié)合三者來談?wù)?code>MVCC的實現(xiàn)从祝,其實也比較簡單襟己,經(jīng)過前面的講解后已得知:

  • ①當(dāng)一個事務(wù)嘗試改動某條數(shù)據(jù)時引谜,會將原本表中的舊數(shù)據(jù)放入Undo-log日志中。
  • ②當(dāng)一個事務(wù)嘗試查詢某條數(shù)據(jù)時尽楔,MVCC會生成一個ReadView快照查蓉。

其中Undo-log主要實現(xiàn)數(shù)據(jù)的多版本惹骂,ReadView則主要實現(xiàn)多版本的并發(fā)控制,還是以之前的例子來舉例說明:

-- 事務(wù)T1:trx_id=1
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
UPDATE `zz_users` SET user_sex = "男" WHERE user_id = 1;
-- 事務(wù)T2:trx_id=2
SELECT * FROM `zz_users` WHERE user_id = 1;

目前存在T1贝室、T2兩個并發(fā)事務(wù),T1目前在修改ID=1的這條數(shù)據(jù)仿吞,而T2則準(zhǔn)備查詢這條數(shù)據(jù)滑频,那么T2在執(zhí)行時具體過程是怎么回事呢?如下:

  • ①當(dāng)事務(wù)中出現(xiàn)select語句時唤冈,會先根據(jù)MySQL的當(dāng)前情況生成一個ReadView峡迷。
  • ②判斷行數(shù)據(jù)中的隱藏列trx_idReadView.creator_trx_id是否相同:
    • 相同:代表創(chuàng)建ReadView和修改行數(shù)據(jù)的事務(wù)是同一個,自然可以讀取最新版數(shù)據(jù)你虹。
    • 不相同:代表目前要查詢的數(shù)據(jù)绘搞,是被其他事務(wù)修改過的,繼續(xù)往下執(zhí)行傅物。
  • ③判斷隱藏列trx_id是否小于ReadView.up_limit_id最小活躍事務(wù)ID
    • 小于:代表改動行數(shù)據(jù)的事務(wù)在創(chuàng)建快照前就已結(jié)束夯辖,可以讀取最新版本的數(shù)據(jù)。
    • 不小于:則代表改動行數(shù)據(jù)的事務(wù)還在執(zhí)行董饰,因此需要繼續(xù)往下判斷蒿褂。
  • ④判斷隱藏列trx_id是否小于ReadView.low_limit_id這個值:
    • 大于或等于:代表改動行數(shù)據(jù)的事務(wù)是生成快照后才開啟的,因此不能訪問最新版數(shù)據(jù)卒暂。
    • 小于:表示改動行數(shù)據(jù)的事務(wù)IDup_limit_id啄栓、low_limit_id之間,需要進一步判斷介却。
  • ⑤如果隱藏列trx_id小于low_limit_id谴供,繼續(xù)判斷trx_id是否在trx_ids中:
    • 在:表示改動行數(shù)據(jù)的事務(wù)目前依舊在執(zhí)行,不能訪問最新版數(shù)據(jù)齿坷。
    • 不在:表示改動行數(shù)據(jù)的事務(wù)已經(jīng)結(jié)束桂肌,可以訪問最新版的數(shù)據(jù)数焊。

說簡單一點,就是首先會去獲取表中行數(shù)據(jù)的隱藏列崎场,然后經(jīng)過上述一系列判斷后佩耳,可以得知:目前查詢數(shù)據(jù)的事務(wù)到底能不能訪問最新版的數(shù)據(jù)。如果能谭跨,就直接拿到表中的數(shù)據(jù)并返回干厚,反之,不能則去Undo-log日志中獲取舊版本的數(shù)據(jù)返回螃宙。

注意:假設(shè)Undo-log日志中存在版本鏈怎么辦蛮瞄?該獲取哪個版本的舊數(shù)據(jù)呢?

如果Undo-log日志中的舊數(shù)據(jù)存在一個版本鏈時谆扎,此時會首先根據(jù)隱藏列roll_ptr找到鏈表頭挂捅,然后依次遍歷整個列表,從而檢索到最合適的一條數(shù)據(jù)并返回堂湖。但在這個遍歷過程中闲先,是如何判斷一個舊版本的數(shù)據(jù)是否合適的呢?條件如下:

  • 舊版本的數(shù)據(jù)无蜂,其隱藏列trx_id不能在ReadView.trx_ids活躍事務(wù)列表中伺糠。

因為如果舊版本的數(shù)據(jù),其trx_id依舊在ReadView.trx_ids中斥季,就代表著產(chǎn)生這條舊數(shù)據(jù)的事務(wù)還未提交训桶,自然不能讀取這個版本的數(shù)據(jù),以前面給出的例子來說明:

這是由事務(wù)T1生成的版本鏈泻肯,此時T2生成的ReadView如下:

{
    "creator_trx_id" : "0",
    "trx_ids" : "[1]",
    "up_limit_id" : "1",
    "low_limit_id" : "2"
}

結(jié)合這個ReadView信息渊迁,經(jīng)過前面那一系列判斷后,最終會得到:不能讀取最新版數(shù)據(jù)灶挟,因此需要去Undo-log的版本鏈中讀數(shù)據(jù)琉朽,首先根據(jù)roll_ptr找到第一條舊數(shù)據(jù):

此時發(fā)現(xiàn)其trx_id=1,位于ReadView.trx_ids中稚铣,因此不能讀取這條舊數(shù)據(jù)箱叁,接著再根據(jù)這條舊數(shù)據(jù)的roll_ptr找到第二條舊版本數(shù)據(jù):

這時再看其trx_id=null,并不位于ReadView.trx_ids中惕医,null表示這條數(shù)據(jù)在上次MySQL運行時就已插入了耕漱,因此這條舊版本的數(shù)據(jù)可以被T2事務(wù)讀取,最終T2就會查詢到這條數(shù)據(jù)并返回抬伺。

OK~螟够,最后再來看一個場景!即范圍查詢時,突然出現(xiàn)新增數(shù)據(jù)怎么辦呢妓笙?如下:

SELECT * FROM `zz_users`;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊貓      | 女       | 6666     | 2022-08-14 15:22:01 |
|       2 | 竹子      | 男       | 1234     | 2022-09-14 16:17:44 |
|       3 | 子竹      | 男       | 4321     | 2022-09-16 07:42:21 |
|       4 | 貓熊      | 女       | 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      | 男       | 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+

-- T1事務(wù):查詢ID >= 3 的所有用戶信息
select * from  `zz_users` where user_id >= 3;

-- T2事務(wù):新增一條 ID = 6 的用戶記錄
INSERT INTO `zz_users` VALUES(6,"棕熊","男","7777","2022-10-02 16:21:33");

此時當(dāng)T1事務(wù)查詢數(shù)據(jù)時若河,突然蹦出來一條ID=6的數(shù)據(jù),經(jīng)過判斷之后會發(fā)現(xiàn)新增這條數(shù)據(jù)的事務(wù)還在執(zhí)行寞宫,所以要去查詢舊版本數(shù)據(jù)萧福,但此時由于是新增操作,因此roll_ptr=null辈赋,即表示沒有舊版本數(shù)據(jù)鲫忍,此時會不會讀取最新版的數(shù)據(jù)呢?答案是NO钥屈,如果查詢數(shù)據(jù)的事務(wù)不能讀取最新版數(shù)據(jù)悟民,同時又無法從版本鏈中找到舊數(shù)據(jù),那就意味著這條數(shù)據(jù)對T1事務(wù)完全不可見焕蹄,因此T1的查詢結(jié)果中不會包含ID=6的這條新增記錄逾雄。

3.5阀溶、RC腻脏、RR不同級別下的MVCC機制

3.4階段已經(jīng)將MVCC機制的具體實現(xiàn)過程剖析了一遍,接下來再思考一個問題:

ReadView是一個事務(wù)中只生成一次银锻,還是每次select時都會生成呢永品?

這個問題的答案跟事務(wù)的隔離機制有關(guān),不同級別的隔離機制也并不同击纬,如果此時MySQL的事務(wù)隔離機制處于RC讀已提交級別鼎姐,那此時來看一個例子:

-- 開啟一個事務(wù)T1:主要是修改兩次ID=1的行數(shù)據(jù)
begin;
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
UPDATE `zz_users` SET user_sex = "男" WHERE user_id = 1;

-- 再開啟一個事務(wù)T2:主要是查詢ID=1的行數(shù)據(jù)
SELECT * FROM `zz_users` WHERE user_id = 1;

-- 此時先提交事務(wù)T1
commit;

-- 再次在事務(wù)T2中查一次ID=1的行數(shù)據(jù)
SELECT * FROM `zz_users` WHERE user_id = 1;

先說明一點,為了方便理解更振,因此我將兩個事務(wù)的代碼貼在了一塊炕桨,但如若你要做實際的實驗,請切記將T1肯腕、T2用兩個連接來寫献宫。

OK~,再來看看上述這個案例实撒,如果是處于RC級別的情況下姊途,T2事務(wù)中的查詢結(jié)果如下:

SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊貓      | 女       | 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 竹子      | 男       | 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

為什么兩次查詢結(jié)果不一樣呢?因為RC級別下知态,MVCC機制是會在每次select語句執(zhí)行前捷兰,都會生成一個ReadView,由于T2事務(wù)中第二次查詢數(shù)據(jù)時负敏,T1已經(jīng)提交了贡茅,所以第二次查詢就能讀到修改后的數(shù)據(jù),這是啥問題?不可重復(fù)讀問題顶考。

接著再來看看RR可重復(fù)級別下的MVCC機制彤叉,SQL代碼和上述一模一樣,但查詢結(jié)果如下:

SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊貓      | 女       | 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊貓      | 女       | 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

這又是為啥村怪?為啥明明在T2事務(wù)第二次查詢前秽浇,T1已經(jīng)提交了,T2依舊查詢出的結(jié)果和第一次相同呢甚负?這是因為在RR級別中柬焕,一個事務(wù)只會在首次執(zhí)行select語句時生成快照,后續(xù)所有的select操作都會基于這個ReadView來判斷梭域,這樣也就解決了RC級別中存在的不可重復(fù)問題斑举。

最后簡單提一嘴:實際上InnoDB引擎中,是可以在RC級別解決臟讀病涨、不可重復(fù)讀富玷、幻讀這一系列問題的,但是為了將事務(wù)隔離級別設(shè)計的符合DBMS規(guī)范既穆,因此在實現(xiàn)時刻意保留了這些問題赎懦,然后放在更高的隔離級別中解決~

四、MVCC機制篇總結(jié)

MVCC多版本并發(fā)控制幻工,聽起來似乎蠻高大上的励两,但實際研究起來會發(fā)現(xiàn)它并不復(fù)雜,其中的多版本主要依賴Undo-log日志來實現(xiàn)囊颅,而并發(fā)控制則通過表的隱藏字段+ReadView快照來實現(xiàn)当悔,通過Undo-log日志、隱藏字段踢代、ReadView快照這三玩意兒盲憎,就實現(xiàn)了MVCC機制,過程還蠻簡單的~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胳挎,一起剝皮案震驚了整個濱河市饼疙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌串远,老刑警劉巖宏多,帶你破解...
    沈念sama閱讀 212,080評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異澡罚,居然都是意外死亡伸但,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評論 3 385
  • 文/潘曉璐 我一進店門留搔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來更胖,“玉大人,你說我怎么就攤上這事∪捶粒” “怎么了饵逐?”我有些...
    開封第一講書人閱讀 157,630評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長彪标。 經(jīng)常有香客問我倍权,道長,這世上最難降的妖魔是什么捞烟? 我笑而不...
    開封第一講書人閱讀 56,554評論 1 284
  • 正文 為了忘掉前任薄声,我火速辦了婚禮,結(jié)果婚禮上题画,老公的妹妹穿的比我還像新娘默辨。我一直安慰自己,他們只是感情好苍息,可當(dāng)我...
    茶點故事閱讀 65,662評論 6 386
  • 文/花漫 我一把揭開白布缩幸。 她就那樣靜靜地躺著,像睡著了一般竞思。 火紅的嫁衣襯著肌膚如雪表谊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,856評論 1 290
  • 那天衙四,我揣著相機與錄音铃肯,去河邊找鬼。 笑死传蹈,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的步藕。 我是一名探鬼主播惦界,決...
    沈念sama閱讀 39,014評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼咙冗!你這毒婦竟也來了沾歪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,752評論 0 268
  • 序言:老撾萬榮一對情侶失蹤雾消,失蹤者是張志新(化名)和其女友劉穎灾搏,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體立润,經(jīng)...
    沈念sama閱讀 44,212評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡狂窑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,541評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了桑腮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泉哈。...
    茶點故事閱讀 38,687評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丛晦,到底是詐尸還是另有隱情奕纫,我是刑警寧澤,帶...
    沈念sama閱讀 34,347評論 4 331
  • 正文 年R本政府宣布烫沙,位于F島的核電站匹层,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏锌蓄。R本人自食惡果不足惜又固,卻給世界環(huán)境...
    茶點故事閱讀 39,973評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望煤率。 院中可真熱鬧仰冠,春花似錦、人聲如沸蝶糯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昼捍。三九已至识虚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妒茬,已是汗流浹背担锤。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留乍钻,地道東北人肛循。 一個月前我還...
    沈念sama閱讀 46,406評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像银择,于是被迫代替她去往敵國和親多糠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,576評論 2 349

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