1. 程式人生 > >【眼見為實】自己動手實踐理解READ COMMITTED && MVCC

【眼見為實】自己動手實踐理解READ COMMITTED && MVCC

開啟 see 查找 學習感悟 鏈表 user 應該 復讀 llb

【眼見為實】自己動手實踐理解 READ COMMITTED && MVCC

首先設置數據庫隔離級別為讀已提交(READ COMMITTED):

set global transaction isolation level READ COMMITTED ;
set session transaction isolation level READ COMMITTED ; 

[READ COMMITTED]能解決的問題

我們來看一下為什麽[READ COMMITTED]如何解決臟讀的問題:
事務1

START TRANSACTION;
① UPDATE users SET state=1
WHERE id=1; ② SELECT sleep(10); ROLLBACK;

事務2

START TRANSACTION;
① SELECT * FROM users WHERE id=1;
COMMIT;

事務1先於事務2執行。
事務1的執行信息

[SQL 1]START TRANSACTION;
受影響的行: 0
時間: 0.001s

[SQL 2]
UPDATE users SET state=1 WHERE id=1;
受影響的行: 1
時間: 0.001s

[SQL 3]
SELECT sleep(10);
受影響的行: 0
時間: 10.000s

[SQL 4]
ROLLBACK;
受影響的行: 0
時間: 0.051s

事務2的執行信息

[SQL 1]START TRANSACTION;
受影響的行: 0
時間: 0.001s

[SQL 2]
SELECT * FROM users WHERE id=1;
受影響的行: 0
時間: 0.005s

[SQL 3]
COMMIT;
受影響的行: 0
時間: 0.001s

最終結果
技術分享圖片
結論
讀已提交[READ COMMITTED]隔離級別可以解決臟讀的問題,但是貌似不是按照二級封鎖協議解決的臟讀問題。

分析
因為讀已提交[READ COMMITTED]隔離級別對應數據庫的二級封鎖協議。二級封鎖協議在修改數據之前對其加X鎖,直到事務結束釋放X鎖。讀數據之前必須加S鎖,讀完即可釋放S鎖。因為事務1先執行修改,修改前申請持有X鎖,事務結束釋放X鎖。持鎖時間段為[SQL 2]開始前到[SQL 4]結束,持鎖時間大約為10.056s。事務2在事務1之後進行讀操作,按照二級封鎖協議所說,事務2在讀數據之前會申請持有S鎖。但是事務1持有此數據的X鎖,所以事務2必須等待事務1釋放X鎖,這個過程大約在10秒左右。但是我們通過事務2的執行信息可以看到執行查詢的時間為0.005s,遠遠小於10秒。所以我們可以大膽推斷Mysql的InnoDB引擎在[READ COMMITTED]隔離級別下對讀操作沒有加鎖

。但是[READ COMMITTED]隔離級別確實解決了臟讀的問題,那麽Mysql是怎麽解決的臟讀問題呢?

MVCC(多版本並發控制)

答案是多版本並發控制(MVCC),可以認為是行級鎖的一個變種,但是它在很多情況下都避免了加鎖操作,因此開銷更低。實現了非堵塞的讀操作,寫操作也只需要鎖定必要的行
如果我們理解了MVCC的工作機制,也就可以理解[READ COMMITTED]隔離級別是如何解決臟讀問題的。

MVCC具體是如下操作的:

SELECT

? InnoDB會根據以下兩個條件檢查記錄:

? ①InnoDB只會查找版本早於當前事務版本的數據行(也就是,行的版本號小於或是等於事務的系統版本 號),這樣可以確保數據讀取的行,要麽是在事務開始前已經存在的,要麽是事務自身插入或修改過的。

? ②行的刪除版本號要麽未定義,要麽大於當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。

? 只有符合上述兩個條件的記錄,才能返回作為查詢結果。

INSERT

? InnoDB為新插入的每一行保存當前系統版本號作為行版本號。

DELETE

? InnoDB為刪除的每一行保存當前系統版本號作為行刪除標識。

UPDATE

? InnoDB為新插入的每一行保存當前系統版本號作為行版本號,同時保存當前系統版本號到原來的行作為行刪除標識。

Innodb為每行記錄都實現了三個隱藏字段:

技術分享圖片
6字節的事務ID(DB_TRX_ID)
7字節的回滾指針(DB_ROLL_PTR)
隱藏的ID 6字節的事物ID用來標識該行所述的事務

事務1會執行如下操作:
①用排他鎖鎖定該行
②記錄redo log
③把該行修改前的值Copy到undo log,即上圖中下面的行
④修改當前行的值,填寫事務編號,使回滾指針指向undo log中的修改前的行
技術分享圖片
如果事務1最後執行COMMIT操作,則什麽操作都不用做。如果執行ROLLBACK操作,則需要通過回滾指針從undo log中還原修改前的數據。

read view 判斷當前版本數據項是否可見

在InnoDB中,創建一個新事務的時候,InnoDB會將當前系統中的活躍事務列表(trx_sys->trx_list)創建一個副本(read view),副本中保存的是系統當前不應該被本事務看到的其他事務id列表。當用戶在這個事務中要讀取該行記錄的時候,InnoDB會將該行當前的版本號與該read view進行比較。
具體的算法如下:

設該行的當前事務id為trx_id,read view中最早的事務id為trx_id_min, 最遲的事務id為trx_id_max。

  • 如果trx_id< trx_id_min的話,那麽表明該行記錄所在的事務已經在本次新事務創建之前就提交了,所以該行記錄的當前值是可見的。
  • 如果trx_id>trx_id_max的話,那麽表明該行記錄所在的事務在本次新事務創建之後才開啟,所以該行記錄的當前值不可見。
  • 如果trx_id_min <= trx_id <= trx_id_max, 那麽表明該行記錄所在事務在本次新事務創建的時候處於活動狀態,從trx_id_min到trx_id_max進行遍歷,如果trx_id等於他們之中的某個事務id的話,那麽不可見。
    從該行記錄的DB_ROLL_PTR指針所指向的回滾段中取出最新的undo-log的版本號的數據,將該可見行的值返回。

需要註意的是,新建事務(當前事務)與正在內存中commit 的事務不在活躍事務鏈表中

對應源代碼如下:

函數:read_view_sees_trx_id。
read_view中保存了當前全局的事務的範圍:
【low_limit_id, up_limit_id】
1. 當行記錄的事務ID小於當前系統的最小活動id,就是可見的。
      if (trx_id < view->up_limit_id) {
        return(TRUE);
      }
2. 當行記錄的事務ID大於當前系統的最大活動id(也就是尚未分配的下一個事務的id),就是不可見的。
      if (trx_id >= view->low_limit_id) {
        return(FALSE);
      }
3. 當行記錄的事務ID在活動範圍之中時,判斷是否在活動鏈表中,如果在就不可見,如果不在就是可見的。
      for (i = 0; i < n_ids; i++) {
        trx_id_t view_trx_id
          = read_view_get_nth_trx_id(view, n_ids - i - 1);
        if (trx_id <= view_trx_id) {
        return(trx_id != view_trx_id);
        }
      }

事務2會執行如下操作:
理想狀態下,事務1的事務id=1,事務2的事務id=2。因為事務2執行時查詢時,事務1正處於等待狀態。所以read view為{1},事務2讀取的數據行 trx_id=1,read view中最早的事務id為trx_id_min=1, 最遲的事務id為trx_id_max=1。因為trx_id_min <= trx_id <= trx_id_max,並且trx_id_min = trx_id = trx_id_max,說明該行記錄所在事務在本次新事務創建的時候處於活動狀態,不可見。所以從該行記錄的DB_ROLL_PTR指針所指向的回滾段中取出最新的undo-log的版本號的數據,將該可見行的值返回。所以不會出現臟讀的現象。

[READ COMMITTED]不能解決的問題

[READ COMMITTED]隔離級別解決不了不可重復讀的問題,一個事務中兩次讀取可能會出現不同的結果。
我們來模擬一下:
事務1

START TRANSACTION;
① SELECT sleep(5);
② UPDATE users SET state=1 WHERE id=1;
COMMIT;

事務2

START TRANSACTION;
① SELECT * FROM users WHERE id=1;
② SELECT sleep(10);
③ SELECT * FROM users WHERE id=1;
COMMIT;  

事務1先於事務2執行。
執行結果
技術分享圖片技術分享圖片
結論
讀已提交[READ COMMITTED]隔離級別不能解決不可重復讀的問題,但是如果按照上面所說,Mysql的InnoDB引擎是通過read view來判斷當前版本數據項是否可見的。那麽讀已提交[READ COMMITTED]隔離級別下應該也不會出現不可重復讀的問題,但是現實並不是。

分析
讀已提交[READ COMMITTED]隔離級別下出現不可重復讀是由於read view的生成機制造成的。在[READ COMMITTED]級別下,只要當前語句執行前已經提交的數據都是可見的。在每次語句執行的過程中,都關閉read view, 重新創建當前的一份read view。這樣就可以根據當前的全局事務鏈表創建read view的事務區間。
那麽在我們模擬的事務中,事務1的事務id trx_id1=1,事務2的事務id trx_id2=2。假設事務2第一次讀取數據前的此行數據的事務id=0。事務2中語句①執行前生成的read view為{1},trx_id_min=1,trx_id_max=1。因為trx_id(0)< trx_id_max(1),此行數據對本次事務可見,將該可見行的值state=0返回。語句①執行後等待10秒,第5秒時事務1對數據加X鎖進行修改操作0->1,然後提交事務釋放鎖。語句②執行前生成的read view為{null},說明當前系統中的不存在其他的活躍事務,也就不存在不應該被本事務看到的其他事務,因此該行記錄的當前值state=1可見。就出現兩次讀取數據不一致的問題,也就是不可重復讀。

不可重復讀的問題在Mysql默認的隔離級別[REPEATABLE READ]中得到了解決。至於是如何解決的,先賣個關子。可以給個小提示,也是和read view的生成機制有關。預知後事如何,請看下篇博客。


本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。

如果您認為還不錯,不妨點擊一下下方的推薦按鈕,謝謝支持。

轉載與引用請註明出處。

【眼見為實】自己動手實踐理解READ COMMITTED && MVCC