一次性掌握innodb引擎如何解決幻讀和不可重複讀

2023-06-19 12:01:16

瞭解mysql的都知道,在mysql的RR(可重複)隔離級別下解決了幻讀和不可重複。你知道RR下是怎麼解決的嗎,很多人會回答是通過MVCC和next-key解決的,具體是怎麼解決的,今天來重點分析下。

mysql的隔離級別都不陌生了,簡單回顧下四種隔離級別:RU(讀未提交)、RC(讀已提交)、RR(可重複讀)【預設隔離級別】、Serializable(可序列化)。四種隔離級別控制力度有淺到深。日常的系統中常使用RC、RR兩種,最常用的是RR。

一、認識兩種讀

說起mysql,很多人會給你說有兩種讀:快照讀和當前讀。我更喜歡在他們前面加個修飾那就是在RC、RR隔離級別下才有快照讀和當前讀。為什麼這樣說,這是因為只有在這兩種隔離級別下才會有兩種讀。其他則是隻有一種讀那就是當前讀。

在RU隔離級別下,不需要進行控制,可以讀到其他事務未提交的事務,那肯定是當前讀了。

在Serializable隔離級別下,所有的sql都會加鎖,查詢型別的sql加S(共用)鎖、更新型別的sql加X(排它)鎖。自然也都是當前讀。

在RC、RR隔離級別下分別要解決髒讀、幻讀和不可重複讀,所以才有了快照讀和當前讀。

 

關於對快照讀和當前讀的理解,從字面意思上理解即可,快照讀也就是會讀取一個快照版本,或者說歷史版本;當前讀就是讀取當前的最新的資料。

在RU、Serializable隔離級別下,普通的select語句都是當前讀;在RC、RR隔離級別下普通的select語句都是快照讀。由於平時使用RR隔離級別多,所以,預設select語句都是快照讀。

不論在哪種隔離級別下更新操作(insert/update/delete)都是當前讀,當前讀是要加鎖的;下面的sql也是當前讀,

select ... for update;  加X(排它)鎖

select ... in share mode; 加S(共用)鎖

二、認識MVCC

MVCC(Multi-Version Concurrency Control)翻譯過來是多版本並行控制,存在於RC、RR隔離級別下,用於快照讀。是這樣來實現的,儲存引擎全域性維護了一個系統版本號,每開啟一個新的事務,這個系統版本號就會遞增。事務開始時刻的系統版本號,會作為這個事務本身的版本號。在每行記錄中,儲存引擎又在每行的後面儲存兩個隱藏的列,分別儲存這一行的開始版本號(trx_id)和過期版本號(roll_pointer)。版本號就是事務ID。

看下各種更新語句版本號的情況,

insert,儲存引擎為新插入的每一行儲存當前的系統版本號作為這一行的開始版本號。

update,儲存引擎會新插入一行記錄,當前的系統版本號是新記錄行的開始版本號;同時會將當前行的過期版本號設為原來行的系統版本號;

delete,儲存引擎將刪除的記錄行的過期版本號設定為當前的系統版本號。

還要了解另外一個概念,readview

readview用來判斷版本鏈中哪個版本對當前事務是可見的,包含4個重要屬性:
  m_ids:生成ReadView時,當前系統活躍的事務id列表;
  min_trx_id:列表中的最小事務id;
  max_trx_id:列表中最大事務id;
  creator_trx_id:生成該ReadView自身事務的id。

注意:如果一個事務唯讀,則creator_trx_id預設為0,只有當事務發生INSERT和UPDATE、DELETE操作時才會分配該事務id。creator_trx_id是為了判斷這條undo log是否是自己生成的。

如何判斷資料的可見性,

  1. 從記錄的最新版本開始迭代依次取隱藏列trx_id和ReadView的m_ids、min_trx_id、max_trx_id比較。
  2. 如果trx_id等於creator_trx_id表示該版本是自己更新的,版本可見;
  3. 如果trx_id在m_ids列表中,則該版本不符合可見性要求;
  4. 如果版本trx_id小於min_trx_id,表示事務在建立ReadView時已經提交,版本可見;
  5. 如果版本trx_id大於max_trx_id表示,該事務是在ReadView生成之後建立和提交的,不符合可見性要求;

簡單點來說,MVCC就是基於記錄的事務id做控制,一條記錄會有多個事務id,代表多個事務id的記錄會儲存在undo log中。

三、解決幻讀和不可重複讀

3.1、select

在RC隔離級別下,解決了髒讀的情況,是怎麼解決的吶,當然是通過MVCC,因為MVCC是基於事務ID,也就是trx_id,所以在select的時候讀取的一定是commit之後的資料,不提交沒有trx_id哦,注意在RC隔離級別下是每次select都會產生一個獨立的readview,所以幻讀和不可重複讀是無法解決的。

在RR隔離級別下,肯定髒讀是不存在的。同時解決了幻讀和不可重複讀,也是通過MVCC,只不過這裡生成readview的時機和RC隔離級別下不一樣,在RR隔離級別下readview是在事務的第一個select的時候生成的,以後的select會使用第一個select的readview,這樣就可以解決了幻讀和不可重複讀。

3.2、update/insert/delete

update/insert/delete語句都是當前讀,為什麼在更新語句中會有讀,這是因為在執行更新操作的時候肯定要先讀出來,再進行更新,這裡的讀便是當前讀,只不過這裡的當前讀在RR隔離級別下是通過加next-key解決的,所以在RR隔離級別下可以解決幻讀和不可重複讀。

總結

本文重點分享了mysql的innodb引擎如何解決幻讀和不可重複讀。重點關注兩點,

1、MVCC,快照讀的時候使用MVCC;

2、next-key,當前讀加next-key;

推薦閱讀

花了一週時間,總算把mysql的加鎖搞清楚了,再也不怕間隙鎖和next-key了

java面試一日一題:講下mysql中的undolog