MVCC 筆記
MVCC為了解決什么問題?
多版本并發控制,針對在并發訪問數據庫時對于數據版本的控制以及隔離性問題,Mysql使用了MVCC的思路來進行版本控制
MVCC的MYSQL 實現淺析?
Mysql 的 MVCC實現大致是通過隱藏列中的DB_ROLL_PTR字段以及undo log的方式生成數據版本鏈,在創建事務時生成ReadView來進行版本比對,從而篩選出當前事務可見的數據行
事務并發執行會遇到的問題?
臟寫(Dirty Write):一個事務修改了另一個事務未提交過的數據
臟讀(Dirty Read):一個事務讀取到了另一個事務未提交過的數據
不可重復讀(Non-Repeatable Read):一個事務只能讀取到另一個已經提交的事務修改過的數據,并且其他事務每對該數據進行一次修改并提交后,該事物都能查到最新的值(在一個事務中的多次查詢,可以查詢到多個其他事務提交的最新值)
幻讀(Phantom):一個事務根據某些條件查詢出一些記錄之后,另一個事務又向表中插入了符合這些條件的記錄,原先的事務用相同的條件再次查詢時,能把另外一個事務插入的數據也查詢出來
按問題嚴重性排序:
臟寫 > 臟讀 > 不可重復讀 > 幻讀
標準的四種 SQL事務隔離級別(并非Mysql定義)
Read UnCommittd:未提交讀
Read Committd:已提交讀
Repeatable Read:可重復讀
Serializable:可串行化
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
---|---|---|---|
Read UnCommittd | Possible | Possible | Possible |
Read Committd | Not Possible | Possible | Possible |
Repeatable Read | Not Possible | Not Possible | Possible |
serializable | Not Possible | Not Possible | Not Possible |
也就是說:
在Read UnCommittd 隔離級別下,可能發生臟讀、不可重復讀和幻讀問題
在Read Committd 隔離級別下,可能發生不可重復讀和幻讀問題
在Repeatable Read 隔離級別下,可能會發生幻讀(但是在Mysql中,Repeatable Read隔離級別可以處理幻讀的問題)
在serializable 隔離級別下,各種問題都不會發生
至于臟寫,應為臟寫實在太嚴重了,所以無論哪個隔離級別都不允許臟寫的情況發生。
什么是版本鏈 ? undo日志 ?
undo日志:用于記錄事務中未提交的變更記錄,主要用于保證事務的原子性,任何對數據的操作都會記錄到undo日志中,直到提交事務或者rollback,才會進行清理。
說起版本鏈,我們得先有行格式的概念,我們大概看一下Compact格式下所看到的一行數據的格式:
在一個正常的行信息中,除了記錄了用戶的真實記錄以外,innoDB還會為每條記錄都添加2個隱藏列以及一個可選列
列名 | 是否必須 | 占用空間 | 描述 |
---|---|---|---|
DB_TRX_ID | 是 | 6字節 | 事務ID |
DB_ROLL_PTR | 是 | 7字節 | 回滾指針 |
DB_ROW_ID | 否 | 6字節 | 行id,唯一標識一條記錄 |
DB_ROW_ID 在沒有自定義主鍵以及存在非Null的Unique鍵時才會添加該列
這里來簡單闡述一下DB_TRX_ID以及DB_ROLL_PTR在版本鏈中的作用
DB_TRX_ID:每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給該記錄的DB_TRX_ID隱藏列,注意事務id是遞增的。
DB_ROLL_PTR:每次對某條聚簇索引記錄改動時,都會將舊的版本寫入到 undo日志中,然后然后這個隱藏列就相當于一個指針,可以通過它來找到該記錄修改前的信息。
注意insert是不會產生DB_ROLL_PRT的,因為insert時并沒有更早的版本存在
了解到這里我們大概就能看到版本鏈的雛形了,也就是利用了DB_ROLL_PTR來鏈接上一個版本的數據;
我們以一個hero表為例:
假如我們有一個hero表其中number為1的記錄name初始化為劉備,我們執行如下兩個語句:
它的版本鏈大概就是下面這個樣子:
每次對該記錄更新后,都會將舊值放到 undo 日志中,隨著更新次數的增多,所有版本都會被DB_ROLL_PRT屬性鏈接成為一個鏈表,我們把這個鏈表稱之為版本鏈,版本鏈的頭節點就是當前記錄最新的值,另外,每個版本中還包含生成該版本時對應的事務id;
ReadView 是什么?
ReadView 可以按字面意思理解為讀視圖,也就是在事務開始時生成的一個快照,ReadView的設計主要是為了解決 "判斷版本鏈中哪個版本是當前事務可見" 的問題
SERIALIZABLE隔離級別采用加鎖的方式來訪問記錄,而READ COMMITTED和 REPEATABLE READ隔離級別在事務的不同階段會創建ReadView
Read committed 隔離級別下,每次讀取數據前都會生產一個ReadView
Repeatable Read 隔離級別下,在第一次讀取數據時生產一個ReadView
關于兩種隔離級別下產生ReadView時機不同帶來的影響,后面描述
ReadView 主要組成結構:
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
只有在對表中的記錄做改動時(執行Insert、update、delete)才會為事務分配事務id,否則在一個只讀事務中,事務id都默認為0
當生成了這個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時生成該版本的事務已經被提交,該版本可以被訪問
Read Committd 每次讀取數據前都生成一個ReadView
假如現在系統中有兩個事務在執行,事務id分別是100、200:
版本鏈如下:
假設現在有一個使用Read Committd隔離級別的事務開始執行:
那么這個Select1的執行過程如下:
在執行SELECT 語句時會現生成一個ReadView,ReadView的m_ids列表內容為[100,200],min_trx_id為100,max_trx_id為201,creator_trx_id為0
然后從版本鏈中挑選可見記錄,從圖中可以看出,最新版本的列name的內容是 '張飛',該版本的事務id為100,在m_ids內,所以不符合可見性要求,根據DB_ROLL_PTR(roll_pointer)找到下一個版本
下一個版本的列name的值為 '關羽',該數據的事務id也為100,在m_ids范圍內,不符合可見性要求,繼續跳到下一個版本
下一個版本的列name的值為 '劉備',該版本的事務id為80,小于ReadView中的min_trx_id值100,所以這個版本符合可見性要求,最終返回給用戶的就是這條name列為 '劉備' 的數據
之后,我們把事務id為100的事務提交一下:
然后再到事務id為200的事務中更新一下hero表中number為1的數據:
此刻表hero中number為1的記錄的版本鏈如下:
然后我們再到剛才使用Read Committd隔離級別的事務中繼續查找這個number為1的記錄,如下:
其中的Select2的執行過程如下:
在執行SELECT2 語句時又會單獨生成一個ReadView,該ReadView的m_ids列表內容是[200](事務id為100的那個事務已經提交了,所以再次生成快照時就沒有它了),min_trx_id為200,max_trx_id為201,creator_id為0
然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name值為 '諸葛亮',該版本的事務id為200,在m_ids列表內,所以不符合可見性要求,根據DB_ROLL_PTR(roll_pointer)跳到下一個版本。
下一個版本的列name的值為 '趙云',該版本的事務id為200,在m_ids列表內,所以也不符合要求,繼續跳到下一個版本
下一個版本的列name的值為 '張飛',該版本的事務id為100,小于ReadView中min_trx_id的值200,所以符合要求,最后返回給用戶的版本就是這條列name為 '張飛' 的記錄
可以看到在Read Committd的隔離級別下,出現了不可重復讀的場景
在Read Committd隔離級別下,事務在每次查詢開始時都會創建一個獨立的ReadView,關于Repeatable Read隔離級別下版本鏈以及執行過程大概類似這里就不闡述了(歡迎討論),只是在Repeatable Read隔離級別下,在事務中多次讀數據時,只會在第一次讀取數據時創建ReadView,后面的查詢都會復用第一次創建的ReadView,這就保證了前后兩次查詢到的結果一致,可以嘗試使用Repeatable Read隔離級別的特性去看看上面的版本鏈,select2在Repeatable Read級別下應該返回什么?怎么去理解可重復度?
總結:
從上邊的描述中我們可以看出來,所謂的MVCC(Multi-Version Concurrency Control ,多版本并發控制)指的就是在使用Read Committd、Repeatable Read這兩種隔離級別的事務在執行普通的SEELCT操作時訪問記錄的版本鏈的過程,這樣子可以使不同事務的讀-寫、寫-讀操作并發執行,從而提升系統性能。Read Committd、Repeatable Read這兩個隔離級別的一個很大不同就是:生成ReadView的時機不同,Read Committd在每一次進行普通SELECT操作前都會生成一個ReadView,而Repeatable Read只在第一次進行普通SELECT操作前生成一個ReadView,之后的查詢操作都重復使用這個ReadView就好了。
疑問?
undo log理論上會在事務提交后進行刪除,那么版本鏈如何形成呢?
實際 insert undo 在事務提交之后就可以被釋放了,update undo由于還需要支持MVCC,不能立即刪除掉,實際在行結構中除了隱藏列還有一個delete mark的標記位,1代表刪除,0代表未刪除,用來記錄數據是否被刪除,所以在上面的版本鏈判斷數據時并非是簡單的判斷事務id,同時還會考慮這個delete_mark標記,同時在mysql中,作者為了減少因為移除數據后的磁盤重新排列的性能問題,還搞了一個所謂的垃圾鏈表,在這個鏈表中的記錄占用的空間稱之為所謂的可重用空間,之后如果有新記錄要插入到表中的話,可能會把這些被刪除記錄占用的存儲空間給覆蓋掉,當然也并非所有被標記了刪除都數據都是覆蓋處理,這里就涉及到mysql的后臺的purge線程的作用了,后面再去了解