一蓖柔、并發(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ù)T1
對ID=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_ID
對purge
線程的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=1
的T1
事務(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-log
的rollback 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_id
與ReadView.creator_trx_id
是否相同:- 相同:代表創(chuàng)建
ReadView
和修改行數(shù)據(jù)的事務(wù)是同一個,自然可以讀取最新版數(shù)據(jù)你虹。 - 不相同:代表目前要查詢的數(shù)據(jù)绘搞,是被其他事務(wù)修改過的,繼續(xù)往下執(zhí)行傅物。
- 相同:代表創(chuàng)建
- ③判斷隱藏列
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ù)
ID
在up_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
機制,過程還蠻簡單的~