(九)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的,那麼咱們下篇見~

「其他文章」