MVCC 是一種強(qiáng)大的并發(fā)控制機(jī)制,在高并發(fā)環(huán)境中起著重要的作用。通過(guò)了解 MVCC 的原理和實(shí)現(xiàn)流程,我們可以更好地理解 MySQL 的并發(fā)控制機(jī)制,理解 MVCC 的原理對(duì)于接觸 MySQL 的開(kāi)發(fā)人員來(lái)說(shuō)是必不可少的知識(shí)點(diǎn)。
摘要
在當(dāng)今高度并發(fā)的數(shù)據(jù)庫(kù)環(huán)境中,有效的并發(fā)控制是至關(guān)重要的。MVCC是MySQL中被廣泛采用的并發(fā)控制機(jī)制,它通過(guò)版本管理來(lái)實(shí)現(xiàn)事務(wù)的隔離性,允許讀寫操作同時(shí)進(jìn)行,提高數(shù)據(jù)庫(kù)的并發(fā)性能和響應(yīng)能力。
本文將深入解析MVCC機(jī)制的原理,幫助讀者更好地理解和應(yīng)用這一關(guān)鍵技術(shù)。
MVCC 介紹MVCC,全稱 Multi-Version Concurrency Control,即多版本并發(fā)控制
MVCC的目的主要是為了提高數(shù)據(jù)庫(kù)并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時(shí),也能做到不加鎖。
這里的多版本指的是數(shù)據(jù)庫(kù)中同時(shí)存在多個(gè)版本的數(shù)據(jù),并不是整個(gè)數(shù)據(jù)庫(kù)的多個(gè)版本,而是某一條記錄的多個(gè)版本同時(shí)存在。
并發(fā)控制的挑戰(zhàn)
在數(shù)據(jù)庫(kù)系統(tǒng)中,同時(shí)執(zhí)行的事務(wù)可能涉及相同的數(shù)據(jù),因此需要一種機(jī)制來(lái)保證數(shù)據(jù)的一致性,傳統(tǒng)的鎖機(jī)制可以實(shí)現(xiàn)并發(fā)控制,但會(huì)導(dǎo)致阻塞和死鎖等問(wèn)題。
MVCC的優(yōu)點(diǎn)
MVCC機(jī)制具有以下優(yōu)點(diǎn):
提高并發(fā)性能:讀操作不會(huì)阻塞寫操作,寫操作也不會(huì)阻塞讀操作,有效地提高數(shù)據(jù)庫(kù)的并發(fā)性能。
降低死鎖風(fēng)險(xiǎn):由于無(wú)需使用顯式鎖來(lái)進(jìn)行并發(fā)控制,MVCC可以降低死鎖的風(fēng)險(xiǎn)。
當(dāng)前讀和快照讀
在講解MVCC原理之前,我們先來(lái)了解一下,當(dāng)前讀和快照讀。
當(dāng)前讀
在MySQL中,當(dāng)前讀是一種讀取數(shù)據(jù)的操作方式,它可以直接讀取最新的數(shù)據(jù)版本,讀取時(shí)還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會(huì)對(duì)讀取的記錄進(jìn)行加鎖。MySQL提供了兩種實(shí)現(xiàn)當(dāng)前讀的機(jī)制:
- 一致性讀(Consistent Read):
默認(rèn)隔離級(jí)別下(可重復(fù)讀),MySQL使用一致性讀來(lái)實(shí)現(xiàn)當(dāng)前讀。
在事務(wù)開(kāi)始時(shí),MySQL會(huì)創(chuàng)建一個(gè)一致性視圖(Consistent View),該視圖反映了事務(wù)開(kāi)始時(shí)刻數(shù)據(jù)庫(kù)的快照。
在事務(wù)執(zhí)行期間,無(wú)論其他事務(wù)對(duì)數(shù)據(jù)進(jìn)行了何種修改,事務(wù)始終使用一致性視圖來(lái)讀取數(shù)據(jù)。
這樣可以保證在同一個(gè)事務(wù)內(nèi)多次查詢返回的結(jié)果是一致的,從而實(shí)現(xiàn)了當(dāng)前讀。
- 鎖定讀(Locking Read):
- 鎖定讀是一種特殊情況下的當(dāng)前讀方式,在某些場(chǎng)景下使用。
- 當(dāng)使用鎖定讀時(shí),MySQL會(huì)在執(zhí)行讀取操作前獲取共享鎖或排他鎖,以確保數(shù)據(jù)的一致性。
- 共享鎖(Shared Lock)允許多個(gè)事務(wù)同時(shí)讀取同一數(shù)據(jù),而排他鎖(Exclusive Lock)則阻止其他事務(wù)讀取或?qū)懭朐摂?shù)據(jù)。
- 鎖定讀適用于需要嚴(yán)格控制并發(fā)訪問(wèn)的場(chǎng)景,但由于加鎖帶來(lái)的性能開(kāi)銷較大,建議僅在必要時(shí)使用。
下面列舉的這些語(yǔ)法都是當(dāng)前讀:
語(yǔ)法 |
SELECT ... LOCK IN SHARE MODE |
SELECT ... FOR UPDATE |
UPDATE |
DELETE |
INSERT |
當(dāng)前讀實(shí)際上是一種加鎖的操作,是悲觀鎖的實(shí)現(xiàn)。
快照讀
快照讀是在讀取數(shù)據(jù)時(shí)讀取一個(gè)一致性視圖中的數(shù)據(jù),MySQL使用 MVCC 機(jī)制來(lái)支持快照讀。
具體而言,每個(gè)事務(wù)在開(kāi)始時(shí)會(huì)創(chuàng)建一個(gè)一致性視圖(Consistent View),該視圖反映了事務(wù)開(kāi)始時(shí)刻數(shù)據(jù)庫(kù)的快照。這個(gè)一致性視圖會(huì)記錄當(dāng)前事務(wù)開(kāi)始時(shí)已經(jīng)提交的數(shù)據(jù)版本。
當(dāng)執(zhí)行查詢操作時(shí),MySQL會(huì)根據(jù)事務(wù)的一致性視圖來(lái)決定可見(jiàn)的數(shù)據(jù)版本。只有那些在事務(wù)開(kāi)始之前已經(jīng)提交的數(shù)據(jù)版本才是可見(jiàn)的,未提交的數(shù)據(jù)或在事務(wù)開(kāi)始后修改的數(shù)據(jù)則對(duì)當(dāng)前事務(wù)不可見(jiàn)。
像不加鎖的 select 操作就是快照讀,即不加鎖的非阻塞讀。
快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本,而有可能是之前的歷史版本。
注意:快照讀的前提是隔離級(jí)別不是串行級(jí)別,在串行級(jí)別下,事務(wù)之間完全串行執(zhí)行,快照讀會(huì)退化為當(dāng)前讀
MVCC主要就是為了實(shí)現(xiàn)讀-寫沖突不加鎖,而這個(gè)讀指的就是快照讀,是樂(lè)觀鎖的實(shí)現(xiàn)。
MVCC 原理解析
隱式字段
MySQL中的行數(shù)據(jù),除了我們?nèi)庋勰芸吹降淖侄沃猓鋵?shí)還包含了一些隱藏字段,它們?cè)趦?nèi)部使用,默認(rèn)情況下不會(huì)顯示給用戶。
字段 |
含義 |
DB_ROW_ID |
隱含的自增ID(隱藏主鍵),用于唯一標(biāo)識(shí)表中的每一行數(shù)據(jù),如果數(shù)據(jù)表沒(méi)有主鍵,InnoDB會(huì)自動(dòng)以DB_ROW_ID產(chǎn)生一個(gè)聚簇索引。 |
DB_TRX_ID |
該字段存儲(chǔ)了當(dāng)前行數(shù)據(jù)所屬的事務(wù)ID。每個(gè)事務(wù)在數(shù)據(jù)庫(kù)中都有一個(gè)唯一的事務(wù)ID。通過(guò) DB_TRX_ID 字段,可以追蹤行數(shù)據(jù)和事務(wù)的所屬關(guān)系。 |
DB_ROLL_PTR |
該字段存儲(chǔ)了回滾指針(Roll Pointer),它指向用于回滾事務(wù)的Undo日志記錄。 |
Undo Log
上文提到了 Undo 日志,這個(gè) Undo 日志是 MVCC 能夠得以實(shí)現(xiàn)的核心所在。
Undo日志(Undo Log)是MySQL中的一種重要的事務(wù)日志,Undo日志的作用主要有兩個(gè)方面:
- 事務(wù)回滾:當(dāng)事務(wù)需要回滾時(shí),MySQL可以通過(guò)Undo日志中的舊值將數(shù)據(jù)還原到事務(wù)開(kāi)始之前的狀態(tài),保證了事務(wù)回滾的一致性。
- MVCC實(shí)現(xiàn):MVCC 是InnoDB存儲(chǔ)引擎的核心特性之一。通過(guò)使用Undo日志,MySQL可以為每個(gè)事務(wù)提供獨(dú)立的事務(wù)視圖,使得事務(wù)讀取數(shù)據(jù)時(shí)能看到一致且符合隔離級(jí)別要求的數(shù)據(jù)版本。
在InnoDB存儲(chǔ)引擎中,Undo日志分為兩種:插入(insert)Undo日志 和 更新(update)Undo日志
- insert undo log:插入U(xiǎn)ndo日志是指在插入操作中生成的Undo日志。由于插入操作的記錄只對(duì)當(dāng)前事務(wù)可見(jiàn),對(duì)其他事務(wù)不可見(jiàn),因此在事務(wù)提交后可以直接刪除,無(wú)需進(jìn)行purge操作。
- update undo log:更新Undo日志是指在更新或刪除操作中生成的Undo日志。更新Undo日志可能需要提供MVCC機(jī)制,因此不能在事務(wù)提交時(shí)就立即刪除。相反,它們會(huì)在提交時(shí)放入U(xiǎn)ndo日志鏈表中,并等待purge線程進(jìn)行最終的刪除。刪除操作只是設(shè)置一下老記錄的 DELETED_BIT,并不真正將過(guò)時(shí)的記錄刪除,為了節(jié)省磁盤空間,InnoDB有專門的purge線程來(lái)清理 DELETED_BIT 為true的記錄。
注意:由于查詢操作(SELECT)并不會(huì)修改任何記錄,所以在查詢操作執(zhí)行時(shí),并不需要記錄相應(yīng)的 undo log 。
不同事務(wù)或者相同事務(wù)對(duì)同一記錄行的修改,會(huì)使該記錄行的 undo log 成為一條鏈表,鏈?zhǔn)拙褪亲钚碌挠涗洠溛簿褪亲钤绲呐f記錄
舉個(gè)例子,比如有個(gè)事務(wù)A插入了一條新記錄:insert into user(id, name) values(1, "小明')
現(xiàn)在來(lái)了一個(gè)事務(wù)B對(duì)該記錄的name做出了修改,改為 "小王"。
在事務(wù)B修改該行數(shù)據(jù)時(shí),數(shù)據(jù)庫(kù)會(huì)先對(duì)該行加排他鎖,然后把該行數(shù)據(jù)拷貝到 undo log 中作為舊記錄,即在 undo log 中有當(dāng)前行的拷貝副本.
拷貝完畢后,修改該行name為 "小王,并且修改隱藏字段的事務(wù)ID為當(dāng)前事務(wù)B的ID, 并將回滾指針指向拷貝到 undo log 的副本記錄,即表示我的上一個(gè)版本就是它,事務(wù)提交后,釋放鎖。
圖片
此時(shí)又來(lái)了個(gè)事務(wù)C修改同一個(gè)記錄,將name修改為 "小紅"。
在事務(wù)C修改該行數(shù)據(jù)時(shí),數(shù)據(jù)庫(kù)也先為該行加鎖,然后把該行數(shù)據(jù)拷貝到 undo log 中,作為舊記錄,發(fā)現(xiàn)該行記錄已經(jīng)有 undo log 了,那么最新的舊數(shù)據(jù)作為鏈表的表頭,插在該行記錄的 undo log 最前面,如下圖:
圖片
關(guān)于 DB_ROLL_PTR 與 Undo日志 的配合工作,具體流程如下:
- 在更新或刪除操作之前,MySQL會(huì)將舊值寫入U(xiǎn)ndo日志中。
- 當(dāng)事務(wù)需要回滾時(shí),MySQL會(huì)根據(jù)事務(wù)的Undo日志記錄,通過(guò) DB_ROLL_PTR 找到對(duì)應(yīng)的Undo日志。
- 根據(jù)Undo日志中記錄的舊值,MySQL將舊值恢復(fù)到相應(yīng)的數(shù)據(jù)行中,實(shí)現(xiàn)數(shù)據(jù)的回滾操作。
比方說(shuō)現(xiàn)在想回滾到事務(wù)B,name值為 "小王" 的時(shí)候,只需通過(guò) DB_ROLL_PTR 順著列表找到對(duì)應(yīng)的 Undo日志,將舊值恢復(fù)到數(shù)據(jù)行即可。
通過(guò) DB_ROLL_PTR 和 Undo日志 的配合工作,MySQL能夠有效地管理事務(wù)的一致性和隔離性。Undo日志的使用也使得MySQL能夠支持MVCC,從而提供了高并發(fā)環(huán)境下的讀取一致性和事務(wù)隔離性。
版本鏈
在MVCC中,對(duì)于每次更新操作,舊值會(huì)被保存到一條undo日志中,即使它是該記錄的舊版本。隨著更新次數(shù)的增加,所有的版本都會(huì)通過(guò)roll_pointer屬性連接成一個(gè)鏈表,稱之為版本鏈。
版本鏈的頭節(jié)點(diǎn)代表當(dāng)前記錄的最新值。此外,每個(gè)版本還包含生成該版本的事務(wù)ID。
Read View
一致性視圖,全稱 Read View ,是用來(lái)判斷版本鏈中的哪個(gè)版本對(duì)當(dāng)前事務(wù)是可見(jiàn)的
Read View 說(shuō)白了就是事務(wù)進(jìn)行快照讀操作時(shí)候生成的讀視圖(Read View),在該事務(wù)執(zhí)行快照讀的那一刻,會(huì)生成數(shù)據(jù)庫(kù)系統(tǒng)當(dāng)前的一個(gè)快照,記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)的ID(每個(gè)事務(wù)開(kāi)啟時(shí),都會(huì)被分配一個(gè)ID,這個(gè)ID是遞增的)。
這里有一點(diǎn)要注意一下:Read View只針對(duì) RC 和 RR級(jí)別
Read Uncommitted(RU)和 Serializable(串行化)是兩個(gè)特殊的隔離級(jí)別,它們不需要使用 Read View 的主要原因是:
- Read Uncommitted(RU)隔離級(jí)別:在 RU 隔離級(jí)別下,事務(wù)可以讀取其他事務(wù)尚未提交的數(shù)據(jù),即臟讀。這意味著不需要通過(guò) Read View 來(lái)限制訪問(wèn)范圍,事務(wù)可以自由地讀取其他事務(wù)的未提交數(shù)據(jù)。由于沒(méi)有對(duì)可見(jiàn)性進(jìn)行嚴(yán)格控制,因此不需要?jiǎng)?chuàng)建或使用 Read View。
- Serializable(串行化)隔離級(jí)別:在 Serializable 隔離級(jí)別下,事務(wù)具有最高的隔離性,確保每次讀取都能看到一致的快照。為了實(shí)現(xiàn)這種隔離級(jí)別,MySQL使用鎖機(jī)制來(lái)保證事務(wù)之間的串行執(zhí)行。由于事務(wù)按順序執(zhí)行,并且不允許并發(fā)操作,所以不需要使用 Read View 進(jìn)行可見(jiàn)性判斷。
Read Uncommitted 和 Serializable 隔離級(jí)別下的事務(wù)規(guī)則不涉及基于 Read View 的可見(jiàn)性判斷。RU 允許臟讀,而 Serializable 則通過(guò)鎖機(jī)制保證串行執(zhí)行。因此,在這兩個(gè)隔離級(jí)別下,不需要?jiǎng)?chuàng)建或使用 Read View。
Read View 可見(jiàn)性原則
Read View 遵循一個(gè)可見(jiàn)性原則,將要被修改的數(shù)據(jù)的 DB_TRX_ID 取出來(lái),與系統(tǒng)當(dāng)前其他活躍事務(wù)的ID去對(duì)比。
如果 DB_TRX_ID 跟 Read View 的屬性做了某些比較,不符合可見(jiàn)性,那就通過(guò) DB_ROLL_PTR 回滾指針去取出 Undo Log 中的 DB_TRX_ID 再比較。
即遍歷鏈表的 DB_TRX_ID (從鏈?zhǔn)椎芥溛玻磸淖罱囊淮涡薷牟槠穑钡秸业綕M足特定條件的 DB_TRX_ID,那么這個(gè) DB_TRX_ID 所在的記錄就是當(dāng)前事務(wù)能看見(jiàn)的最新老版本。
Read View 會(huì)維護(hù)以下幾個(gè)字段:
字段 |
含義 |
m_ids |
|
m_creator_trx_id |
創(chuàng)建該 |
m_low_limit_id |
目前出現(xiàn)過(guò)的最大的事務(wù) ID+1,即下一個(gè)將被分配的事務(wù) ID。大于等于這個(gè) ID 的數(shù)據(jù)版本均不可見(jiàn)。 |
m_up_limit_id |
活躍事務(wù)列表 |
Read View 可見(jiàn)性具體判斷如下:
- 如果被訪問(wèn)版本的 DB_TRX_ID 屬性值與 Read View 中的 m_creator_trx_id 值相同,表示當(dāng)前事務(wù)正在訪問(wèn)自己所修改的記錄,因此該版本可以被當(dāng)前事務(wù)訪問(wèn)。
- 如果被訪問(wèn)版本的 DB_TRX_ID 屬性值小于 Read View 中的 m_up_limit_id 值,說(shuō)明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 Read View 之前已經(jīng)提交,因此該版本可以被當(dāng)前事務(wù)訪問(wèn)。
- 如果被訪問(wèn)版本的 DB_TRX_ID 屬性值大于或等于 Read View 中的 m_low_limit_id 值,說(shuō)明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 Read View 之后才提交,因此該版本不能被當(dāng)前事務(wù)訪問(wèn)。
- 如果被訪問(wèn)版本的 DB_TRX_ID 屬性值位于 Read View 的 m_up_limit_id 和 m_low_limit_id 之間(包括邊界),則需要進(jìn)一步檢查 DB_TRX_ID 是否在m_ids 列表中。如果在列表中,說(shuō)明在創(chuàng)建ReadView時(shí)生成該版本的事務(wù)仍處于活躍狀態(tài),因此該版本不能被訪問(wèn);如果不在列表中,說(shuō)明在創(chuàng)建 Read View 時(shí)生成該版本的事務(wù)已經(jīng)提交,因此該版本可以被訪問(wèn)。
事務(wù)可見(jiàn)性示意圖:
圖片
RC 和 RR 下的 Read View
RC 和 RR 下生成 Read View 的時(shí)機(jī)是有所差異的:
- RC:每次 SELECT 數(shù)據(jù)前都生成一個(gè)ReadView。
- RR:只在第一次讀取數(shù)據(jù)時(shí)生成一個(gè)ReadView,后面會(huì)復(fù)用第一次生成的。
正因?yàn)镽C 和 RR生成 Read View 的時(shí)機(jī)不同,導(dǎo)致兩個(gè)級(jí)別下看到的數(shù)據(jù)會(huì)不一致。
舉例說(shuō)明,假設(shè)數(shù)據(jù)初始狀態(tài)如下:
有 A,B,C 三個(gè)事務(wù),執(zhí)行順序如下:
|
事務(wù)A(事務(wù)ID: 100) |
事務(wù)B(事務(wù)ID: 200) |
事務(wù)C(事務(wù)ID: 300) |
T1 |
begin |
|
|
T2 |
|
begin |
begin |
T3 |
update user set name="小王" where id=1 |
|
|
T4 |
update user set name="小紅" where id=1 |
|
select * from user where id = 1 |
T5 |
commit |
update user set name="小黑" where id=1 |
|
T6 |
|
update user set name="小白" where id=1 |
select * from user where id = 1 |
T7 |
|
commit |
|
T8 |
|
|
select * from user where id = 1 |
T9 |
|
|
commit |
T10 |
|
|
|
RC 下的 Read View
T4時(shí)刻
我們來(lái)看 T4 時(shí)刻的情況,此時(shí) 事務(wù)A 和 事務(wù)B 都還沒(méi)提交,所以活躍的事務(wù)ID,即 m_ids 為:[100,200],四個(gè)字段的值分別如下:
字段 |
值 |
m_ids |
[100,200] |
m_creator_trx_id |
300 |
m_low_limit_id |
400 |
m_up_limit_id |
100 |
T4時(shí)刻的版本鏈如下:
圖片
依據(jù)我們之前說(shuō)的可見(jiàn)性原則,事務(wù)C最終看到的應(yīng)該是 name = "小明" 的數(shù)據(jù),理由如下:
最新記錄的 DB_TRX_ID 為 100,既不小于 m_up_limit_id,也不大于 m_low_limit_id,也不等于 m_creator_trx_id。
落在了黃區(qū):
圖片
DB_TRX_ID 存在于 m_ids 列表中,故不可見(jiàn),順著版本鏈繼續(xù)往下。
根據(jù) DB_ROLL_PTR 找到 undo log 中的前一版本記錄,前一條記錄的 DB_TRX_ID 還是 100,還是不可見(jiàn),繼續(xù)往下。
繼續(xù)找前一條 DB_TRX_ID為 1,滿足 1 < m_up_limit_id,可見(jiàn),所以事務(wù)C 查詢到數(shù)據(jù)為 name = "小明" 。
T6時(shí)刻
T6時(shí)候的版本鏈如下:
圖片
T6時(shí)刻,會(huì)再次生成新的 Read View,四個(gè)字段的值分別如下:
字段 |
值 |
m_ids |
[200] |
m_creator_trx_id |
300 |
m_low_limit_id |
400 |
m_up_limit_id |
200 |
根據(jù)可見(jiàn)性原則,最終T6時(shí)刻事務(wù)C 查詢到數(shù)據(jù)為 name = "小紅" 。
T8時(shí)刻
T8時(shí)刻的版本鏈和T6時(shí)刻是一致的,不同的是 Read View,因?yàn)門8時(shí)刻會(huì)再生成一個(gè) Read View,四個(gè)字段的值分別如下:
字段 |
值 |
m_ids |
[] |
m_creator_trx_id |
300 |
m_low_limit_id |
400 |
m_up_limit_id |
400 |
根據(jù)可見(jiàn)性原則,最終T8時(shí)刻事務(wù)C 查詢到數(shù)據(jù)為 name = "小白" 。
總結(jié)一下,事務(wù)C在 RC 級(jí)別下各個(gè)時(shí)刻看到的數(shù)據(jù)如下:
時(shí)刻 |
name |
T4 |
小明 |
T6 |
小紅 |
T8 |
小白 |
下面我們來(lái)看看,RR 級(jí)別下的表現(xiàn)是如何的。
RR 下的 Read View
(RR 的版本鏈和 RC 的版本鏈?zhǔn)且恢碌模瑓^(qū)別在于 Read View)
T4時(shí)刻
T4 時(shí)刻的情況,和 R C的情況是一致的:
字段 |
值 |
m_ids |
[100,200] |
m_creator_trx_id |
300 |
m_low_limit_id |
400 |
m_up_limit_id |
100 |
根據(jù)可見(jiàn)性原則,最終T4時(shí)刻事務(wù)C 查詢到數(shù)據(jù)為 name = "小明" ,和 RC 的T4時(shí)刻是一致的。
T6時(shí)刻
RR 級(jí)別會(huì)復(fù)用 Read View,所以T6時(shí)刻也是:
字段 |
值 |
m_ids |
[100,200] |
m_creator_trx_id |
300 |
m_low_limit_id |
400 |
m_up_limit_id |
100 |
根據(jù)可見(jiàn)性原則,T6時(shí)刻我們發(fā)現(xiàn)事務(wù)C查詢到的數(shù)據(jù)還是 name = "小明" 。
繼續(xù)看T8時(shí)刻。
T8時(shí)刻
T8時(shí)刻繼續(xù)復(fù)用先前的 Read View。
根據(jù)可見(jiàn)性原則,T8時(shí)刻事務(wù)C查詢到的數(shù)據(jù)依舊是 name = "小明" 。
小結(jié)
我們將事務(wù)C在 RC 和 RR 級(jí)別下看到的數(shù)據(jù),放到一塊來(lái)對(duì)比下:
時(shí)刻 |
RC |
RR |
T4 |
小明 |
小明 |
T6 |
小紅 |
小明 |
T8 |
小白 |
小明 |
可以看出二者由于生成 Read View 的時(shí)機(jī)不同,導(dǎo)致在各個(gè)時(shí)刻看到的數(shù)據(jù)會(huì)存在差異。
回過(guò)頭來(lái)看 RC 和 RR 隔離級(jí)別的定義,會(huì)有種恍然大悟的感覺(jué):
- 讀已提交(Read Committed):事務(wù)只能讀取到已經(jīng)提交的數(shù)據(jù)。
- 可重復(fù)讀(Repeatable Read):事務(wù)在整個(gè)事務(wù)期間保持一致的快照視圖,不受其他事務(wù)的影響。
總之在 RC 隔離級(jí)別下,每個(gè)快照讀都會(huì)生成并獲取最新的 Read View;而在 RR 隔離級(jí)別下,則是只在第一個(gè)快照讀創(chuàng)建Read View,之后的快照讀獲取的都是同一個(gè)Read View
RR 級(jí)別下能否防止幻讀
嚴(yán)謹(jǐn)?shù)恼f(shuō),RR 級(jí)別下只能防止部分幻讀
首先,幻讀通常指的是在同一個(gè)事務(wù)中,第二次查詢發(fā)現(xiàn)了新增加的行,而第一次查詢并沒(méi)有返回這些新增加的行。
通過(guò)前面的例子,我們也看到了,在 RR 隔離級(jí)別下,由于一致性視圖的存在,如果其他事務(wù)插入了新的行,在同一個(gè)事務(wù)中進(jìn)行多次查詢,這些新增的行將會(huì)被包含在事務(wù)的一致性視圖中,確實(shí)可以避免部分幻讀場(chǎng)景。
這里注意一下:MVCC解決的只是 RR 級(jí)別下快照讀的幻讀問(wèn)題,而當(dāng)前讀的幻讀問(wèn)題則是通過(guò)臨鍵鎖來(lái)解決的。也就是說(shuō) RR 級(jí)別下是通過(guò) MVCC+臨鍵鎖 來(lái)解決大部分幻讀問(wèn)題的。
為什么說(shuō)是部分解決?看下面這個(gè)例子:
|
事務(wù)A |
事務(wù)B |
T1 |
begin |
|
T2 |
|
begin |
T3 |
|
select * from user |
T4 |
insert into user(id, name) values(2, "小張') |
|
T5 |
|
select * from user for update |
T6 |
commit |
|
T7 |
|
commit |
假設(shè)數(shù)據(jù)初始狀態(tài)如下:
圖片
T3時(shí)刻看到的數(shù)據(jù)只有一條 name = "小明",而T5時(shí)刻,由于 select * from user for update 使用的是當(dāng)前讀,讀取的是最新的數(shù)據(jù)版本,T5時(shí)刻查詢出來(lái)的數(shù)據(jù)是兩條,name 分別為 "小明" 和 "小張"。
理解了上面的例子之后,再看下面這個(gè)例子:
|
事務(wù)A |
事務(wù)B |
T1 |
begin |
|
T2 |
|
begin |
T3 |
|
select * from user |
T4 |
insert into user(id, name) values(2, "小張') |
|
T5 |
|
update user set name="小陳" where id=2 |
T6 |
|
select * from user |
T7 |
commit |
|
T8 |
|
commit |
UPDATE 語(yǔ)句也是當(dāng)前讀,也會(huì)發(fā)生幻讀問(wèn)題,最終看到的數(shù)據(jù)是name 分別為 "小明" 和 "小陳"。
這里發(fā)生幻讀的原因,和上面的例子是一樣的,本質(zhì)都是在一個(gè)事務(wù)中,即使用了快照讀又使用了當(dāng)前讀,RR 級(jí)別下無(wú)法預(yù)防此種情況,所以說(shuō) RR 級(jí)別下無(wú)法完全解決幻讀問(wèn)題。
總結(jié)
綜上所述,MVCC 是一種強(qiáng)大的并發(fā)控制機(jī)制,在高并發(fā)環(huán)境中起著重要的作用。通過(guò)了解 MVCC 的原理和實(shí)現(xiàn)流程,我們可以更好地理解 MySQL 的并發(fā)控制機(jī)制,理解 MVCC 的原理對(duì)于接觸 MySQL 的開(kāi)發(fā)人員來(lái)說(shuō)是必不可少的知識(shí)點(diǎn)。