MySQL之MVCC初探(1)
- 2019 年 11 月 6 日
- 筆記
MySQL之MVCC初探(1)
//
MVCC初探—結合案例
昨天的文章中,我們說了MVCC的基本概念,然後講了記錄額外的兩個欄位,今天我們通過例子來說明一下MVCC在實際應用中的表現。我們首先創建一張表,然後插入一條數據:
mysql:yeyztest 21:48:49>>show create table swordsmanG *************************** 1. row *************************** Table: swordsman Create Table: CREATE TABLE `swordsman` ( `id` int() NOT NULL AUTO_INCREMENT, `name` varchar() DEFAULT NULL, `kongfu` varchar() DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT= DEFAULT CHARSET=utf8 row in set (0.00 sec) mysql:yeyztest ::>>select * from swordsman; +----+-----------+-----------------+ | id | name | kongfu | +----+-----------+-----------------+ | 1 | 張無忌 | 乾坤大挪移 | +----+-----------+-----------------+ 1 row in set (0.00 sec)
這裡我們插入一條數據,這條數據有三個欄位,分別id,名稱和武功,我們看一下這條記錄是如何存儲的:

可以看到,除了已有的三個欄位外,後面還有兩個欄位,分別是trx_id和roll_pointer,這兩個欄位就是我們昨天說的隱藏列,其中trx_id保存的是記錄的創建版本號,roll_pointer裡面是一個指針,它指向了一個insert的undo日誌,這個日誌中保存的是之前的版本號。這裡需要注意,這個undo日誌只在事務執行的過程中存在,一旦這個insert事務提交完畢了,這條insert undo日誌也就不存在了,系統會自動回收。
在現在這個情況下,我們假設對這條記錄做一個改動,先看看改動的先後順序:

這裡我們看到兩個事務對這個記錄進行了更改,其中第一個事務的事務id是20,第二個事務的事務id是30,此時這個記錄就變成了:
mysql:yeyztest 22:12:07>>select * from swordsman; +----+--------+-----------------+ | id | name | kongfu | +----+--------+-----------------+ | 1 | 楊逍 | 乾坤大挪移 | +----+--------+-----------------+ 1 row in set (0.00 sec)
這很好理解,因為第二個事務的提交時間比第一個晚,所以最終的name的值變成了"楊逍",這個時候,這條記錄其實已經有很多版本了。畫出來就是:

其實也就是之前的trx_id為10和20的版本也都存在於這個版本鏈裡面了。如果我們對這條記錄再次進行更新,這個版本鏈將會越來越多,這個版本鏈表的頭結點就是當前最新的記錄值。這樣隨著版本越來越多,一個重要的問題就出現了:在並發事務的時候,如何判斷到底當前版本鏈中的哪一個版本對當前事務是可見的。
這個也是多版本並發控制的核心,我們知道事務有四個隔離級別,其中Read uncommitted級別是可以讀取到其他未提交事務修改的記錄,最高級別的serializable事務來說,innodb中是通過加鎖的方式來訪問記錄的,暫時不考慮這種情況,這樣一來,我們只需要考慮在RC隔離級別和RR隔離級別下的情況了。
這裡我們提出一個讀視圖的概念,也可以稱之為ReadView,在這個概念中,包含4個比較重要的內容,分別是:
1、m_ids:表示在生成readview時當前系統中活躍的讀寫事務的事務id列表 2、min_trx_id:m_ids中的最小值 3、max_trx_id:表示生成readview時系統中應該分配給下一個事務的id值 4、creator_trx_id:表示生成該ReadView的事務的事務id,默認值為0
這裡需要補充重要的一點:事務執行過程中,只有在第一次真正修改記錄時(比如使用INSERT、DELETE、UPDATE語句),才會被分配一個單獨的事務id。
有了這四個概念和readview的概念,我們可以根據下面的方法來判斷記錄的某個版本是否可見:
第一條:如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當前事務在訪問它自己修改過的記錄,所以該版本可以被當前事務訪問。 第二條:如果被訪問版本的trx_id屬性值小於ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以該版本可以被當前事務訪問。 第三條:如果被訪問版本的trx_id屬性值大於ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView後才開啟,所以該版本不可以被當前事務訪問。 第四條:如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明創建ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明創建ReadView時生成該版本的事務已經被提交,該版本可以被訪問。
這樣說可能比較拗口,我們看下例子吧,假設現在有一個RC隔離級別的事務開始執行,事務id為20:

然後再開啟一個事務id為30的事務,對別的表執行一個update操作,目的是為了生成版本號。

此時我們先把版本鏈畫在這裡:

這裡需要注意的是藍色部分的trx_id之所以為20,是因為在trx 20裡面更新了兩次name的值,而在trx 30中還沒有對name的值進行更新。
此時我們在開啟第3個事務,事務id為40,並在事務中執行select * from swordsman where id=1;

此時我們思考一下,這個事務讀取到的值應該是多少?
分析過程如下:
1、這個select的語句會生成一個ReadView,其中m_ids的列表裡面有事務id為20和30的兩條記錄,也就是m_ids[20,30],min_trx_id=20,max_trx_id=31(30的下一個事務id是31),creator_trx_id=0(默認值)
2、從版本鏈中,我們套用之前的四條規則,可以發現最新值是陽頂天,版本值是20,在m_ids裡面,所以不符合規則當中的第四條。接著往下找
3、下一個版本的name列是楊逍,trx id是20,在m_ids裡面,所以不符合第四條要求,繼續跳到下一個版本
4、下一個版本的name值是"張無忌",trx id的值是10,符合可見性規則的第2條,10<20,說明生成該版本的事務在當前事務生成ReadView前已經提交,所以該版本可以被當前事務訪問。
所以這個select的值就是trx id為10的那條記錄,也就是: +—-+———–+—————–+ | id | name | kongfu | +—-+———–+—————–+ | 1 | 張無忌 | 乾坤大挪移 | +—-+———–+—————–+
為了避免混亂,我們今天先寫這麼多,改天再謝謝RC隔離級別和RR隔離級別的區別,今天的實驗是在RC隔離級別下的。