MySQL歸納總結InnoDB之MVCC原理

2022-04-18 19:00:15
本篇文章給大家帶來了關於的相關知識,其中主要介紹了關於InnoDB之MVCC原理的相關問題,MVCC即多版本並行控制,主要是為了提高資料庫的並行效能,下面一起來看一下,希望對大家有幫助。

推薦學習:

MVCC全稱Multi-Version Concurrency Control,即多版本並行控制,主要是為了提高資料庫的並行效能。同一行資料平時發生讀寫請求時,會上鎖阻塞住。但MVCC用更好的方式去處理讀—寫請求,做到在發生讀—寫請求衝突時不用加鎖。這個讀是指的快照讀,而不是當前讀,當前讀是一種加鎖操作,是悲觀鎖。那它到底是怎麼做到讀—寫不用加鎖的,快照讀和當前讀是指什麼?我們後面都會學到。

MySQL在REPEATABLE READ隔離級別下,是可以很大程度避免幻讀問題的發生的,MySQL是怎麼做到的?

版本鏈

我們知道,對於使用InnoDB儲存引擎的表來說,它的聚簇索引記錄中都包含兩個必要的隱藏列(row_id並不是必要的,我們建立的表中有主鍵或者非NULL的UNIQUE鍵時都不會包含row_id列):

  • trx_id:每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給trx_id隱藏列。

  • roll_pointer:每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到undo紀錄檔中,然後這個隱藏列就相當於一個指標,可以通過它來找到該記錄修改前的資訊。

為了說明這個問題,我們建立一個演示表:

CREATE TABLE `teacher` (
  `number` int(11) NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  `domain` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`number`)) ENGINE=InnoDB DEFAULT CHARSET=utf8

然後向這個表裡插入一條資料:

mysql> insert into teacher values(1, 'J', 'Java');Query OK, 1 row affected (0.01 sec)

現在裡的資料就是這樣的:

mysql> select * from teacher;
+--------+------+--------+
| number | name | domain |
+--------+------+--------+
|      1 | J    | Java   |
+--------+------+--------+
1 row in set (0.00 sec)

假設插入該記錄的事務id為60,那麼此刻該條記錄的示意圖如下所示:

image-20220108095128853

假設之後兩個事務id分別為80、120的事務對這條記錄進行UPDATE操作,操作流程如下:

Trx80Trx120
begin

begin
update teacher set name=‘S’ where number=1;
update teacher set name=‘T’ where number=1;
commit

update teacher set name=‘K’ where number=1;

update teacher set name=‘F’ where number=1;

commit

每次對記錄進行改動,都會記錄一條undo紀錄檔,每條undo紀錄檔也都有一個roll_pointer屬性(INSERT操作對應的undo紀錄檔沒有該屬性,因為該記錄並沒有更早的版本),可以將這些undo紀錄檔都連起來,串成一個連結串列,所以現在的情況就像下圖一樣:

image-20220108095413957

對該記錄每次更新後,都會將舊值放到一條undo紀錄檔中,就算是該記錄的一箇舊版本,隨著更新次數的增多,所有的版本都會被roll_pointer屬性連線成一個連結串列,我們把這個連結串列稱之為版本鏈,版本鏈的頭節點就是當前記錄最新的值。另外,每個版本中還包含生成該版本時對應的事務id。於是可以利用這個記錄的版本鏈來控制並行事務存取相同記錄的行為,那麼這種機制就被稱之為多版本並行控制(Mulit-Version Concurrency Control MVCC)。

ReadView

對於使用READ UNCOMMITTED隔離級別的事務來說,由於可以讀到未提交事務修改過的記錄,所以直接讀取記錄的最新版本就好了。

對於使用SERIALIZABLE隔離級別的事務來說,InnoDB使用加鎖的方式來存取記錄。

對於使用READ COMMITTED和REPEATABLE READ隔離級別的事務來說,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:READ COMMITTED和REPEATABLE READ隔離級別在不可重複讀和幻讀上的區別,這兩種隔離級別關鍵是需要判斷一下版本鏈中的哪個版本是當前事務可見的。

為此,InnoDB提出了一個ReadView的概念,這個ReadView中主要包含4個比較重要的內容:

  • m_ids:表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表。

  • min_trx_id:表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。

  • max_trx_id:表示生成ReadView時系統中應該分配給下一個事務的id值。注意max_trx_id並不是m_ids中的最大值,事務id是遞增分配的。比方說現在有id為1,2,3這三個事務,之後id為3的事務提交了。那麼一個新的讀事務在生成ReadView時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。

  • creator_trx_id:表示生成該ReadView的事務的事務id。

有了這個ReadView,這樣在存取某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:

  1. 如果被存取版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當前事務在存取它自己修改過的記錄,所以該版本可以被當前事務存取。
  2. 如果被存取版本的trx_id屬性值小於ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以該版本可以被當前事務存取。
  3. 如果被存取版本的trx_id屬性值大於或等於ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView後才開啟,所以該版本不可以被當前事務存取。
  4. 如果被存取版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間(min_trx_id <= trx_id < max_trx_id),那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明建立ReadView時生成該版本的事務還是活躍的,事務還沒提交,該版本不可以被存取;如果不在,說明建立ReadView時生成該版本的事務已經被提交,該版本可以被存取。
  5. 如果某個版本的資料對當前事務不可見的話,那就順著版本鏈找到下一個版本的資料,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最後一個版本。如果最後一個版本也不可見的話,那麼就意味著該條記錄對該事務完全不可見,查詢結果就不包含該記錄。

在MySQL中,READ COMMITTED和REPEATABLE READ隔離級別的的一個非常大的區別就是它們生成ReadView的時機不同。

我們還是以表teacher為例,假設現在表teacher中只有一條由事務id為60的事務插入的一條記錄,接下來看一下READ COMMITTED和REPEATABLE READ所謂的生成ReadView的時機不同到底不同在哪裡。

READ COMMITTED每次讀取資料前都生成一個ReadView

假設現在系統裡有兩個事務id分別為80、120的事務在執行:

# Transaction 80
set session transaction isolation level read committed;
begin
update teacher set name='S' where number=1;
update teacher set name='T' where number=1;

此刻,表teacher中number為1的記錄得到的版本連結串列如下所示:

image-20220108100036522

假設現在有一個使用READ COMMITTED隔離級別的事務開始執行:

set session transaction isolation level read committed;
# 使用READ COMMITTED隔離級別的事務
begin;
# SELECE1:Transaction 80、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'J'

這個SELECE1的執行過程如下:

  • 在執行SELECT語句時會先生成一個ReadView,ReadView的m_ids列表的內容就是[80, 120],min_trx_id為80,max_trx_id為121,creator_trx_id為0。

  • 然後從版本鏈中挑選可見的記錄,最新版本的列name的內容是’T’,該版本的trx_id值為80,在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。

  • 下一個版本的列name的內容是’S’,該版本的trx_id值也為80,也在m_ids列表內,根據步驟4也不符合要求,繼續跳到下一個版本。

  • 下一個版本的列name的內容是’J’,該版本的trx_id值為60,小於ReadView 中的min_trx_id值,根據步驟2判斷這個版本是符合要求的。

之後,我們把事務id為80的事務提交一下,然後再到事務id為120的事務中更新一下表teacher 中number為1的記錄:

set session transaction isolation level read committed;
# Transaction 120
begin
update teacher set name='K' where number=1;
update teacher set name='F' where number=1;

此刻,表teacher 中number為1的記錄的版本鏈就長這樣:

image-20220108100456504

然後再到剛才使用READ COMMITTED隔離級別的事務中繼續查詢這個number 為1的記錄,如下:

# 使用READ COMMITTED隔離級別的事務
begin;
# SELECE1:Transaction 80、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'J'
# SELECE2:Transaction 80提交、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'T'

這個SELECE2 的執行過程如下:

  • 在執行SELECT語句時會又會單獨生成一個ReadView,該ReadView的m_ids列表的內容就是[120](事務id為80的那個事務已經提交了,所以再次生成快照時就沒有它了),min_trx_id為120,max_trx_id為121,creator_trx_id為0。
  • 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是’F’,該版本的trx_id值為120,在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。
  • 下一個版本的列name 的內容是’K’,該版本的trx_id值為120,也在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。
  • 下一個版本的列name的內容是’T’,該版本的trx_id值為80,小於ReadView中的min_trx_id值120,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以這個版本是符合要求的,最後返回給使用者的版本就是這條列name為’‘T’'的記錄。

以此類推,如果之後事務id為120的記錄也提交了,再次在使用READCOMMITTED隔離級別的事務中查詢表teacher中number值為1的記錄時,得到的結果就是’F’了,具體流程我們就不分析了。

總結一下就是:使用READCOMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的ReadView。

REPEATABLE READ —— 在第一次讀取資料時生成一個ReadView

對於使用REPEATABLE READ隔離級別的事務來說,只會在第一次執行查詢語句時生成一個ReadView,之後的查詢就不會重複生成了。我們還是用例子看一下是什麼效果。

假設現在系統裡有兩個事務id分別為80、120的事務在執行:

# Transaction 80
begin
update teacher set name='S' where number=1;
update teacher set name='T' where number=1;

此刻,表teacher中number為1的記錄得到的版本連結串列如下所示:

image-20220108100036522

假設現在有一個使用REPEATABLE READ隔離級別的事務開始執行:

# 使用REPEATABLE READ隔離級別的事務
begin;
# SELECE1:Transaction 80、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'J'

這個SELECE1的執行過程如下(與READ COMMITTED的過程一致):

  • 在執行SELECT語句時會先生成一個ReadView,ReadView的m_ids列表的內容就是[80, 120],min_trx_id為80,max_trx_id為121,creator_trx_id為0。

  • 然後從版本鏈中挑選可見的記錄,最新版本的列name的內容是’T’,該版本的trx_id值為80,在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。

  • 下一個版本的列name的內容是’S’,該版本的trx_id值也為80,也在m_ids列表內,根據步驟4也不符合要求,繼續跳到下一個版本。

  • 下一個版本的列name的內容是’J’,該版本的trx_id值為60,小於ReadView 中的min_trx_id值,根據步驟2判斷這個版本是符合要求的。

之後,我們把事務id為80的事務提交一下,然後再到事務id為120的事務中更新一下表teacher 中number為1的記錄:

# Transaction 80
begin
update teacher set name='K' where number=1;
update teacher set name='F' where number=1;

此刻,表teacher 中number為1的記錄的版本鏈就長這樣:

image-20220108100456504

然後再到剛才使用REPEATABLE READ隔離級別的事務中繼續查詢這個number為1的記錄,如下:

# 使用REPEATABLE READ隔離級別的事務
begin;
# SELECE1:Transaction 80、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'J'
# SELECE2:Transaction 80提交、120未提交
SELECT * FROM teacher WHERE number = 1; # 得到的列name的值為'J'

這個SELECE2的執行過程如下:

  • 因為當前事務的隔離級別為REPEATABLE READ,而之前在執行SELECE1時已經生成過ReadView了,所以此時直接複用之前的ReadView,之前的ReadView的m_ids列表的內容就是[80, 120],min_trx_id為80,max_trx_id為121,creator_trx_id為0。
  • 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是’F’,該版本的trx_id值為120,在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。
  • 下一個版本的列name的內容是’K’,該版本的trx_id值為120,也在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。
  • 下一個版本的列name的內容是’T’,該版本的trx_id值為80,也在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。
  • 下一個版本的列name的內容是’S’,該版本的trx_id值為80,也在m_ids列表內,根據步驟4不符合可見性要求,根據roll_pointer跳到下一個版本。
  • 下一個版本的列name的內容是’J’,該版本的trx_id值為60,小於ReadView中的min_trx_id值80,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以這個版本是符合要求的,最後返回給使用者的版本就是這條列name為’‘J’'的記錄。

也就是說兩次SELECT查詢得到的結果是重複的,記錄的列值都是’’‘J’’’,這就是可重複讀的含義。

如果我們之後再把事務id為120的記錄提交了,然後再到剛才使用REPEATABLE READ隔離級別的事務中繼續查詢這個number為1的記錄,得到的結果還是’J’,具體執行過程大家可以自己分析一下。

MVCC下的幻讀現象和幻讀解決

前面我們已經知道了,REPEATABLE READ隔離級別下MVCC可以解決不可重複讀問題,那麼幻讀呢?MVCC是怎麼解決的?幻讀是一個事務按照某個相同條件多次讀取記錄時,後讀取時讀到了之前沒有讀到的記錄,而這個記錄來自另一個事務新增的新記錄。

我們可以想想,在REPEATABLE READ隔離級別下的事務T1先根據某個搜尋條件讀取到多條記錄,然後事務T2插入一條符合相應搜尋條件的記錄並提交,然後事務T1再根據相同搜尋條件執行查詢。結果會是什麼?按照ReadView中的比較規則:

不管事務T2比事務T1是否先開啟,事務T1都是看不到T2的提交的。請自行按照上面介紹的版本鏈、ReadView以及判斷可見性的規則來分析一下。

但是,在REPEATABLE READ隔離級別下InnoDB中的MVCC可以很大程度地避免幻讀現象,而不是完全禁止幻讀。怎麼回事呢?我們來看下面的情況:

T1T2
begin;
select * from teacher where number=30; 無資料begin;

insert into teacher values(30, ‘X’, ‘Java’);

commit;
update teacher set domain=‘MQ’ where number=30;
select * from teacher where number = 30; 有資料

嗯,怎麼回事?事務T1很明顯出現了幻讀現象。在REPEATABLE READ隔離級別下,T1第一次執行普通的SELECT語句時生成了一個ReadView,之後T2向teacher表中新插入一條記錄並提交。ReadView並不能阻止T1執行UPDATE或者DELETE語句來改動這個新插入的記錄(由於T2已經提交,因此改動該記錄並不會造成阻塞),但是這樣一來,這條新記錄的trx_id隱藏列的值就變成了T1的事務id。之後T1再使用普通的SELECT語句去查詢這條記錄時就可以看到這條記錄了,也就可以把這條記錄返回給使用者端。因為這個特殊現象的存在,我們也可以認為MVCC並不能完全禁止幻讀。

MVCC小結

從上邊的描述中我們可以看出來,所謂的MVCC(Multi-Version ConcurrencyControl ,多版本並行控制)指的就是在使用READ COMMITTD、REPEATABLE READ這兩種隔離級別的事務在執行普通的SELECT操作時存取記錄的版本鏈的過程,這樣子可以使不同事務的讀-寫、寫-讀操作並行執行,從而提升系統效能。

READ COMMITTD、REPEATABLE READ這兩個隔離級別的一個很大不同就是:生成ReadView的時機不同,READ COMMITTD在每一次進行普通SELECT操作前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操作前生成一個ReadView,之後的查詢操作都重複使用這個ReadView就好了,從而基本上可以避免幻讀現象。

我們之前說執行DELETE語句或者更新主鍵的UPDATE語句並不會立即把對應的記錄完全從頁面中刪除,而是執行一個所謂的delete mark操作,相當於只是對記錄打上了一個刪除標誌位,這主要就是為MVCC服務的。另外,所謂的MVCC只是在我們進行普通的SEELCT查詢時才生效,截止到目前我們所見的所有SELECT語句都算是普通的查詢,至於什麼是個不普通的查詢,後面就會講到。

推薦學習:

以上就是MySQL歸納總結InnoDB之MVCC原理的詳細內容,更多請關注TW511.COM其它相關文章!