(九)MySQL之MVCC機制:為什麼你改了的資料我還看不見?

語言: CN / TW / HK

theme: channing-cyan

引言

本文為掘金社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

   在《MySQL鎖機制》這篇文章中,咱們全面剖析了MySQL提供的鎖機制,對於併發事務通常可以通過其提供的各類鎖,去確保各場景下的執行緒安全問題,從而能夠防止髒寫、髒讀、不可重複讀及幻讀這類問題出現。

不過成也蕭何敗也蕭何,雖然MySQL提供的鎖機制確實能解決併發事務帶來的一系列問題,但由於加鎖後會讓一部分事務序列化,而MySQL本身就是基於磁碟實現的,效能無法跟記憶體型資料庫娉美,因此併發事務序列化會使其效率更低。

也正是由於上述原因,因此MySQL官方在設計時,抓破腦袋的想:有沒有辦法再快一點!!最終,MVCC機制就誕生了,相較於加鎖序列化執行,MVCC機制的出現,則以另一種形式解決了併發事務造成的問題。

一、併發事務的四種場景

   併發事務中又會分為四種情況,分別是讀-讀、寫-寫、讀-寫、寫-讀,這四種情況分別對應併發事務執行時的四種場景,為了後續分析MVCC機制時方便理解,因此先將這幾種情況說明,咱們首先來看看讀-讀場景。

1.1、讀-讀場景

   讀-讀場景即是指多個事務/執行緒在一起讀取一個相同的資料,比如事務T1正在讀取ID=88的行記錄,事務T2也在讀取這條記錄,兩個事務之間是併發執行的。

廣為人知的一點:MySQL執行查詢語句,絕對不會對引起資料的任何變化,因此對於這種情況而言,不需要做任何操作,因為不改變資料就不會引起任何併發問題。

1.2、寫-寫場景

   寫-寫場景也比較簡單,也就是指多個事務之間一起對同一資料進行寫操作,比如事務T1ID=88的行記錄做修改操作,事務T2則對這條資料做刪除操作,事務T1提交事務後想查詢看一下,哦豁,結果連這條資料都不見了,這也是所謂的髒寫問題,也被稱為更新覆蓋問題,對於這個問題在所有資料庫、所有隔離級別中都是零容忍的存在,最低的隔離級別也要解決這個問題。

1.3、讀-寫、寫-讀場景

   讀-寫、寫-讀實際上從巨集觀角度來看,可以理解成同一種類型的操作,但從微觀角度而言則是兩種不同的情況,讀-寫是指一個事務先開始讀,然後另一個事務則過來執行寫操作,寫-讀則相反,主要是讀、寫發生的前後順序的區別。

併發事務中同時存在讀、寫兩類操作時,這是最容易出問題的場景,髒讀、不可重複讀、幻讀都出自於這種場景中,當有一個事務在做寫操作時,讀的事務中就有可能出現這一系列問題,因此資料庫才會引入各種機制解決。

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

   在《MySQL鎖機制》中,對於寫-寫、讀-寫、寫-讀這三類場景,都是利用加鎖的方案確保執行緒安全,但上面說到過,加鎖會導致部分事務序列化,因此效率會下降,而MVCC機制的誕生則解決了這個問題。

先來設想一個問題:加鎖的目的是什麼?防止髒寫、髒讀、不可重複讀及幻讀這類問題出現。

對於髒寫問題,這是寫-寫場景下會出現的,寫-寫場景必須要加鎖才能保障安全,因此先將該場景排除在外。再想想:對於讀-寫並存的場景中,髒讀、不可重複讀及幻讀問題都出自該場景中,但實際專案中,出現這些問題的機率本身就比較小,為了防止一些小概念事件,就將所有操縱同一資料的併發讀寫事務序列化,這似乎有些不講道理呀,就好比:

為了防止自家保險櫃中的3.25元被偷,所以每天從早到晚一直守著保險櫃,這合理嗎?並不合理,畢竟只有千日做賊,那有千日防賊的道理。

因此MySQL就基於讀-寫並存的場景,推出了MVCC機制,線上程安全問題和加鎖序列化之間做了一定取捨,讓兩者之間達到了很好的平衡,即防止了髒讀、不可重複讀及幻讀問題的出現,又無需對併發讀-寫事務加鎖處理。

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

二、MySQL-MVCC機制綜述

   MVCC機制的全稱為Multi-Version Concurrency Control,即多版本併發控制技術,主要是為了提升資料庫併發效能而設計的,其中採用更好的方式處理了讀-寫併發衝突,做到即使有讀寫衝突時,也可以不加鎖解決,從而確保了任何時刻的讀操作都是非阻塞的。

但與其說是MySQL-MVCC機制,還不如說是InnoDB-MVCC機制,因為在MySQL眾多的開源儲存引擎中,幾乎只有InnoDB實現了MVCC機制,類似於MyISAM、Memory等引擎中都未曾實現,那其他引擎為何不實現呢?不是不想,而是做不到,這跟MVCC機制的實現原理有關,這點放在後續詳細講解~

不過為了更好的理解啥叫MVCC多版本併發控制,先來看一個日常生活的例子~

2.1、MVCC技術在日常生活中的體現

   不知道各位小夥伴中,是否有人做過論壇這類業務的專案,或者類似稽核的業務需求,以掘金的文章為例,此時來思考一個場景:

假設我釋出了一篇關於《MySQL事務機制》的文章,釋出後挺受歡迎的,因此有不少小夥伴在看,其中有一位小夥伴比較細心,文中存在兩三個錯別字,被這位小夥伴指出來了,因此我去修正錯別字後重新發布。

問題來了,對於文章首次釋出也好,重新發布也罷,絕對要等稽核通過後才會正式釋出的,那我修正文章後重新發布,文章又會進入「稽核中」這個狀態,此時對於其他正在看、準備看的小夥伴來說,文章是不是就不見了?畢竟文章還在稽核撒,因此對這個業務需求又該如何實現呢?多版本!

啥意思呢?也就是說,對於首次釋出後通過稽核的文章,在後續重新發布稽核時,使用者可以看到更新前的文章,也就是看到老版本的文章,當更新後的文章稽核通過後,再使用新版本的文章代替老版本的文章即可。

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

2.2、MySQL-MVCC多版本併發控制

   MySQL中的多版本併發控制,也和上面給出的例子類似,畢竟回想一下,髒讀、不可重複讀、幻讀問題都是由於多個事務併發讀寫導致的,但這些問題都是基於最新版本的資料併發操作才會出現,那如果讀、寫的事務操作的不是同一個版本呢?比如寫操作走新版本,讀操作走老版本,這樣是不是無論執行寫操作的事務幹了啥,都不會影響讀的事務?答案是Yes

不過要稍微記住,MySQL中僅在RC讀已提交級別、RR可重複讀級別才會使用MVCC機制,Why

因為如果是RU讀未提交級別,既然都允許存在髒讀問題、允許一個事務讀取另一個事務未提交的資料,那自然可以直接讀最新版本的資料,因此無需MVCC介入。

同時如若是Serializable序列化級別,因為會將所有的併發事務序列化處理,也就是不論事務是讀操作,亦或是寫操作,都會被排好隊一個個執行,這都不存在所謂的多執行緒併發問題了,自然也無需MVCC介入。

因此要牢記:MVCC機制在MySQL中,僅有InnoDB引擎支援,而在該引擎中,MVCC機制只對RC、RR兩個隔離級別下的事務生效。當然,RC、RR兩個不同的隔離級別中,MVCC的實現也存在些許差異,對於這點後續詳細講解。

三、MySQL-MVCC機制實現原理剖析

   OK~,簡單理解了啥叫MVCC機制後,接著一起來看看InnoDB引擎是如何實現它的,MVCC機制主要通過隱藏欄位、Undo-log日誌、ReadView這三個東西實現的,因而這三玩意兒也被稱為“MVCC三劍客”!廢話不多說,一起來看看。

3.1、InnoDB表的隱藏欄位

   通常而言,當你基於InnoDB引擎建立一張表後,MySQL除開會構建你顯式宣告的欄位外,通常還會構建一些InnoDB引擎的隱藏欄位,在InnoDB引擎中主要有DB_ROW_ID、DB_Deleted_Bit、DB_TRX_ID、DB_ROLL_PTR這四個隱藏欄位,挨個簡單介紹一下。

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

在之前介紹《索引原理篇》的時候聊到過一點,對於InnoDB引擎的表而言,由於其表資料是按照聚簇索引的格式儲存,因此通常都會選擇主鍵作為聚簇索引列,然後基於主鍵欄位構建索引樹,但如若表中未定義主鍵,則會選擇一個具備唯一非空屬性的欄位,作為聚簇索引的欄位來構建樹。

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

因此要牢記一點,如果你選擇的引擎是InnoDB,就算你的表中未定義主鍵、索引,其實預設也會存在一個聚簇索引,只不過這個索引在上層無法使用,僅提供給InnoDB構建樹結構儲存表資料。

3.1.2、刪除標識 - Deleted_Bit(1Bytes)

在之前講《SQL執行篇-寫SQL執行原理》時,咱們只粗略的過了一下大體流程,其中並未涉及到一些細節闡述,在這裡稍微提一下:對於一條delete語句而言,當執行後並不會立馬刪除表的資料,而是將這條資料的Deleted_Bit刪除標識改為1/true,後續的查詢SQL檢索資料時,如果檢索到了這條資料,但看到隱藏欄位Deleted_Bit=1時,就知道該資料已經被其他事務delete了,因此不會將這條資料納入結果集。

OK~,但設計Deleted_Bit這個隱藏欄位的好處是什麼呢?主要是能夠有利於聚簇索引,比如當一個事務中刪除一條資料後,後續又執行了回滾操作,假設此時是真正的刪除了表資料,會發生什麼情況呢?

  • ①刪除表資料時,有可能會破壞索引樹原本的結構,導致出現葉子節點合併的情況。
  • ②事務回滾時,又需重新插入這條資料,再次插入時又會破壞前面的結構,導致葉子節點分裂。

綜上所述,如果執行delete語句就刪除真實的表資料,由於事務回滾的問題,就很有可能導致聚簇索引樹發生兩次結構調整,這其中的開銷可想而知,而且先刪除,再回滾,最終樹又變成了原狀,那這兩次樹的結構調整還是無意義的。

所以,當執行delete語句時,只會改變將隱藏欄位中的刪除標識改為1/true,如果後續事務出現回滾動作,直接將其標識再改回0/false即可,這樣就避免了索引樹的結構調整。

但如若事務刪除資料之後提交了事務呢?總不能讓這條資料一直留在磁碟吧?畢竟如果所有的delete操作都這麼幹,就會導致磁碟爆滿~,顯然這樣是不妥的,因此刪除標識為1/true的資料最終依舊會從磁碟中移除,啥時候移呢?

在之前講《Nginx-快取清理》時,曾經提到過purger這一系列的引數,通過配置該系列引數後,Nginx後臺中會建立對應的purger執行緒去自動刪除快取資料。而MySQL中也不例外,同樣存在purger執行緒的概念,為了防止“已刪除”的資料佔用過多的磁碟空間,purger執行緒會自動清理Deleted_Bit=1/true的行資料。

當然,為了確保清理資料時不會影響MVCC的正常工作,purger執行緒自身也會維護一個ReadView,如果某條資料的Deleted_Bit=true,並且TRX_IDpurge執行緒的ReadView可見,那麼這條資料一定是可以被安全清除的(即不會影響MVCC工作)。

對於上述最後一段大家可能會有些許疑惑,這是因為還未曾介紹ReadView,因此有些不理解可先跳過,後續理解了ReadView後再回來看會好很多。

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

TRX_ID全稱為transaction_id,翻譯過來也就是事務ID的意思,MySQL對於每一個建立的事務,都會為其分配一個事務ID,事務ID同樣遵循順序遞增的特性,即後來的事務ID絕對會比之前的ID要大,比如:

此時事務T1準備修改表字段的值,MySQL會為其分配一個事務ID=1,當事務T2準備向表中插入一條資料時,又會為這個事務分配一個ID=2......

但有一個細節點需要記住:MySQL對於所有包含寫入SQL的事務,會為其分配一個順序遞增的事務ID,但如果是一條select查詢語句,則分配的事務ID=0

不過對於手動開啟的事務,MySQL都會為其分配事務ID,就算這個手動開啟的事務中僅有select操作。

表中的隱藏欄位TRX_ID,記錄的就是最近一次改動當前這條資料的事務ID,這個欄位是實現MVCC機制的核心之一。

3.1.4、回滾指標 - ROLL_PTR(7Bytes)

ROLL_PTR全稱為rollback_pointer,也就是回滾指標的意思,這個也是表中每條資料都會存在的一個隱藏欄位,當一個事務對一條資料做了改動後,都會將舊版本的資料放到Undo-log日誌中,而rollback_pointer就是一個地址指標,指向Undo-log日誌中舊版本的資料,當需要回滾事務時,就可以通過這個隱藏列,來找到改動之前的舊版本資料,而MVCC機制也利用這點,實現了行資料的多版本。

3.2、InnoDB引擎的Undo-log日誌

   在之前《事務篇》中分析事務實現原理時,咱們得知了MySQL事務機制是基於Undo-log實現的,同時在剛剛在聊回滾指標時,聊到了Undo-log日誌中會儲存舊版本的資料,但要注意:Undo-log中並不僅僅只儲存一條舊版本資料,其實在該日誌中會有一個版本鏈,啥意思呢?舉個例子: ``sql SELECT * FROMzz_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事務,其中對同一條資料改動了兩次,那Undo-log日誌中只會儲存一條舊版本資料嗎?NO,答案是兩條舊版本的資料,如下圖: ![Undo版本鏈](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/57440100409d482ab5df6ccd8554714d~tplv-k3u1fbpfcp-watermark.image?) 從上圖中可明顯看出:不同的舊版本資料,會以roll_ptr`回滾指標作為連結點,然後將所有的舊版本資料組成一個單向連結串列。但要注意一點:最新的舊版本資料,都會插入到連結串列頭中,而不是追加到連結串列尾部。

細說一下執行上述update語句的詳細過程:
①對ID=1這條要修改的行資料加上排他鎖。
②將原本的舊資料拷貝到Undo-logrollback Segment區域。
③對錶資料上的記錄進行修改,修改完成後將隱藏欄位中的trx_id改為當前事務ID
④將隱藏欄位中的roll_ptr指向Undo-log中對應的舊資料,並在提交事務後釋放鎖。

為什麼Undo-log日誌要設計出版本鏈呢?兩個好處:一方面可以實現事務點回滾(這點回去參考事務篇),另一方面則可以實現MVCC機制(這點後面聊)。

與之前的刪除標識類似,一條資料被delete後並提交了,最終會從磁碟移除,而Undo-log中記錄的舊版本資料,同樣會佔用空間,因此在事務提交後也會移除,移除的工作同樣由purger執行緒負責,purger執行緒內部也會維護一個ReadView,它會以此作為判斷依據,來決定何時移除Undo記錄。

3.3、MVCC核心 - ReadView

   MVCC在前面聊到過,它翻譯過來就是多版本併發控制的意思,對於這個名詞中的多版本已經通過Undo-log日誌實現了,但再思考一個問題:如果T2事務要查詢一條行資料,此時這條行資料正在被T1事務寫,那也就代表著這條資料可能存在多箇舊版本資料,T2事務在查詢時,應該讀這條資料的哪個版本呢?此時就需要用到ReadView,用它來做多版本的併發控制,根據查詢的時機來選擇一個當前事務可見的舊版本資料讀取。

那究竟什麼是ReadView呢?就是一個事務在嘗試讀取一條資料時,MVCC基於當前MySQL的執行狀態生成的快照,也被稱之為讀檢視,即ReadView,在這個快照中記錄著當前所有活躍事務的ID(活躍事務是指還在執行的事務,即未結束(提交/回滾)的事務)。

當一個事務啟動後,首次執行select操作時,MVCC就會生成一個數據庫當前的ReadView,通常而言,一個事務與一個ReadView屬於一對一的關係(不同隔離級別下也會存在細微差異),ReadView一般包含四個核心內容:
- creator_trx_id:代表建立當前這個ReadView的事務ID。 - trx_ids:表示在生成當前ReadView時,系統內活躍的事務ID列表。 - up_limit_id:活躍的事務列表中,最小的事務ID。 - low_limit_id:表示在生成當前ReadView時,系統中要給下一個事務分配的ID值。

上面四個值很簡單,值得一提的是low_limit_id,它並不是目前系統中活躍事務的最大ID,因為之前講到過,MySQL的事務ID是按序遞增的,因此當啟動一個新的事務時,都會為其分配事務ID,而這個low_limit_id則是整個MySQL中,要為下一個事務分配的ID值。

下面上個ReadView的示意圖,來好好理解一下它:
ReadView
假設目前資料庫中共有T1~T5這五個事務,T1、T2、T4還在執行,T3已經回滾,T5已經提交,此時當有一條查詢語句執行時,就會利用MVCC機制生成一個ReadView,由於前面講過,單純由一條select語句組成的事務並不會分配事務ID,因此預設為0,所以目前這個快照的資訊如下:
json { "creator_trx_id" : "0", "trx_ids" : "[1,2,4]", "up_limit_id" : "1", "low_limit_id" : "6" } OK~,簡單明白ReadView的結構後,接著一起來聊一聊MVCC機制的實現原理。

3.4、MVCC機制實現原理

   將“MVCC三劍客”的概念闡述完畢後,再結合三者來談談MVCC的實現,其實也比較簡單,經過前面的講解後已得知: - ①當一個事務嘗試改動某條資料時,會將原本表中的舊資料放入Undo-log日誌中。 - ②當一個事務嘗試查詢某條資料時,MVCC會生成一個ReadView快照。

其中Undo-log主要實現資料的多版本,ReadView則主要實現多版本的併發控制,還是以之前的例子來舉例說明:
sql -- 事務T1:trx_id=1 UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1; UPDATE `zz_users` SET user_sex = "男" WHERE user_id = 1;

sql -- 事務T2:trx_id=2 SELECT * FROM `zz_users` WHERE user_id = 1; 目前存在T1、T2兩個併發事務,T1目前在修改ID=1的這條資料,而T2則準備查詢這條資料,那麼T2在執行時具體過程是怎麼回事呢?如下: - ①當事務中出現select語句時,會先根據MySQL的當前情況生成一個ReadView。 - ②判斷行資料中的隱藏列trx_idReadView.creator_trx_id是否相同: - 相同:代表建立ReadView和修改行資料的事務是同一個,自然可以讀取最新版資料。 - 不相同:代表目前要查詢的資料,是被其他事務修改過的,繼續往下執行。 - ③判斷隱藏列trx_id是否小於ReadView.up_limit_id最小活躍事務ID: - 小於:代表改動行資料的事務在建立快照前就已結束,可以讀取最新版本的資料。 - 不小於:則代表改動行資料的事務還在執行,因此需要繼續往下判斷。 - ④判斷隱藏列trx_id是否小於ReadView.low_limit_id這個值: - 大於或等於:代表改動行資料的事務是生成快照後才開啟的,因此不能訪問最新版資料。 - 小於:表示改動行資料的事務IDup_limit_id、low_limit_id之間,需要進一步判斷。 - ⑤如果隱藏列trx_id小於low_limit_id,繼續判斷trx_id是否在trx_ids中: - 在:表示改動行資料的事務目前依舊在執行,不能訪問最新版資料。 - 不在:表示改動行資料的事務已經結束,可以訪問最新版的資料。

說簡單一點,就是首先會去獲取表中行資料的隱藏列,然後經過上述一系列判斷後,可以得知:目前查詢資料的事務到底能不能訪問最新版的資料。如果能,就直接拿到表中的資料並返回,反之,不能則去Undo-log日誌中獲取舊版本的資料返回。

注意:假設Undo-log日誌中存在版本鏈怎麼辦?該獲取哪個版本的舊資料呢?

如果Undo-log日誌中的舊資料存在一個版本鏈時,此時會首先根據隱藏列roll_ptr找到連結串列頭,然後依次遍歷整個列表,從而檢索到最合適的一條資料並返回。但在這個遍歷過程中,是如何判斷一箇舊版本的資料是否合適的呢?條件如下: - 舊版本的資料,其隱藏列trx_id不能在ReadView.trx_ids活躍事務列表中。

因為如果舊版本的資料,其trx_id依舊在ReadView.trx_ids中,就代表著產生這條舊資料的事務還未提交,自然不能讀取這個版本的資料,以前面給出的例子來說明:
Undo版本鏈
這是由事務T1生成的版本鏈,此時T2生成的ReadView如下:
json { "creator_trx_id" : "0", "trx_ids" : "[1]", "up_limit_id" : "1", "low_limit_id" : "2" } 結合這個ReadView資訊,經過前面那一系列判斷後,最終會得到:不能讀取最新版資料,因此需要去Undo-log的版本鏈中讀資料,首先根據roll_ptr找到第一條舊資料:
第一條舊資料
此時發現其trx_id=1,位於ReadView.trx_ids中,因此不能讀取這條舊資料,接著再根據這條舊資料的roll_ptr找到第二條舊版本資料:
第二條舊資料
這時再看其trx_id=null,並不位於ReadView.trx_ids中,null表示這條資料在上次MySQL執行時就已插入了,因此這條舊版本的資料可以被T2事務讀取,最終T2就會查詢到這條資料並返回。

OK~,最後再來看一個場景!即範圍查詢時,突然出現新增資料怎麼辦呢?如下:

``sql SELECT * FROMzz_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事務:查詢ID >= 3 的所有使用者資訊 select * from zz_users where user_id >= 3;

-- T2事務:新增一條 ID = 6 的使用者記錄 INSERT INTO zz_users VALUES(6,"棕熊","男","7777","2022-10-02 16:21:33"); `` 此時當T1事務查詢資料時,突然蹦出來一條ID=6的資料,經過判斷之後會發現新增這條資料的事務還在執行,所以要去查詢舊版本資料,但此時由於是新增操作,因此roll_ptr=null,即表示沒有舊版本資料,此時會不會讀取最新版的資料呢?答案是NO,如果查詢資料的事務不能讀取最新版資料,同時又無法從版本鏈中找到舊資料,那就意味著這條資料對T1事務完全不可見,因此T1的查詢結果中不會包含ID=6`的這條新增記錄。

3.5、RC、RR不同級別下的MVCC機制

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

ReadView是一個事務中只生成一次,還是每次select時都會生成呢?

這個問題的答案跟事務的隔離機制有關,不同級別的隔離機制也並不同,如果此時MySQL的事務隔離機制處於RC讀已提交級別,那此時來看一個例子:
``sql -- 開啟一個事務T1:主要是修改兩次ID=1的行資料 begin; UPDATEzz_usersSET user_name = "竹子" WHERE user_id = 1; UPDATEzz_users` SET user_sex = "男" WHERE user_id = 1;

-- 再開啟一個事務T2:主要是查詢ID=1的行資料 SELECT * FROM zz_users WHERE user_id = 1;

-- 此時先提交事務T1 commit;

-- 再次在事務T2中查一次ID=1的行資料 SELECT * FROM zz_users WHERE user_id = 1; ```

先說明一點,為了方便理解,因此我將兩個事務的程式碼貼在了一塊,但如若你要做實際的實驗,請切記將T1、T2用兩個連線來寫。

OK~,再來看看上述這個案例,如果是處於RC級別的情況下,T2事務中的查詢結果如下:
``sql SELECT * FROMzz_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 | +---------+-----------+----------+----------+---------------------+ `` 為什麼兩次查詢結果不一樣呢?因為RC級別下,MVCC機制是會在每次select語句執行前,都會生成一個ReadView,由於T2事務中第二次查詢資料時,T1`已經提交了,所以第二次查詢就能讀到修改後的資料,這是啥問題?不可重複讀問題。

接著再來看看RR可重複級別下的MVCC機制,SQL程式碼和上述一模一樣,但查詢結果如下: ``sql SELECT * FROMzz_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事務第二次查詢前,T1已經提交了,T2依舊查詢出的結果和第一次相同呢?這是因為在RR級別中,一個事務只會在首次執行select語句時生成快照,後續所有的select操作都會基於這個ReadView來判斷,這樣也就解決了RC`級別中存在的不可重複問題。

最後簡單提一嘴:實際上InnoDB引擎中,是可以在RC級別解決髒讀、不可重複讀、幻讀這一系列問題的,但是為了將事務隔離級別設計的符合DBMS規範,因此在實現時刻意保留了這些問題,然後放在更高的隔離級別中解決~

四、MVCC機制篇總結

   MVCC多版本併發控制,聽起來似乎蠻高大上的,但實際研究起來會發現它並不複雜,其中的多版本主要依賴Undo-log日誌來實現,而併發控制則通過表的隱藏欄位+ReadView快照來實現,通過Undo-log日誌、隱藏欄位、ReadView快照這三玩意兒,就實現了MVCC機制,過程還蠻簡單的~

到這裡,其實對於MySQL的事務隔離機制,已經撥開一部分迷霧了,下篇《MySQL事務與鎖機制原理篇》中,則會徹底講清楚MySQL鎖是怎麼實現的,以及不同的事務隔離級別,又是如何藉助鎖+MVCC處理客戶端SQL的,那麼咱們下篇見~

「其他文章」