1.首先了解下什麼是read view
這裏說的 read view 是InnoDB 在實現 MVCC 時用到的一致性讀視圖,即 consistent read view,用於支持 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別的實現。
read view 並沒有物理結構,作用是事務執行期間用來定義"我能看到什麼數據"。
2.事務id
InnoDB 裏面每個事務有一個唯一的事務 ID,叫做 transaction id。它是在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。
在innodb存儲引擎下,聚簇索引記錄中都包含兩個必要的隱藏列:
trx_id:每次對某條記錄進行改動時,對會把對應的事務id賦值給trx_id隱藏列;
roll_pointer:每次對某條記錄進行改動時,這個隱藏列會存一個指針,可以通過這個指針找到該記錄修改前的信息,也就是undo回滾段中的內容。
3.RR(Repeatable Read,可重複讀)隔離級別下的實現
在可重複讀隔離級別下,事務在啓動的時候就"拍了個快照"。注意,這個快照是基於整庫的。
以一個事務啓動的時刻爲準,如果一個數據版本是在這個事務啓動之前生成的,就可以看到;如果是在這個事務啓動以後才生成的,就看不到,就必須要找到它的上一個版本。
實現上,事務啓動的瞬間,InnoDB 爲這個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正在"活躍"的所有事務 ID。“活躍”指的就是,啓動了但還沒提交。數組裏面事務 ID 的最小值記爲低水位,當前系統裏面已經創建過的事務 ID 的最大值加 1 記爲高水位。這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view),不同時刻啓動的事務會有不同的 read-view。
而每次事務更新數據的時候,都會生成一個新的數據版本,並且把 transaction id 賦值給這個數據版本的事務ID,記爲 row trx_id。同時,舊的數據版本要保留,並且在新的數據版本中,能夠有信息可以直接拿到它。
這個read view 視圖數組把所有的 row trx_id 分成了幾種不同的情況:已提交事務、未提交事務集合、未開始事務。當前事務一定在低水位和高水位之間。
那麼總結下來可以有幾個概念就是:
m_ids:表示在生成readview時,當前系統中活躍的讀寫事務id數組;
min_trx_id:表示在生成readview時,當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中最小的值,就是低水位;
max_trx_id:表示生成readview時,系統中應該分配給下一個事務的id值,就是高水位;
creator_trx_id:表示生成該readview的事務的事務id;
而數據版本的可見性規則,就是基於數據的 row trx_id 和這個一致性視圖的對比結果得到的:
1.如果被訪問版本的row trx_id,與readview中的creator_trx_id值相同,表明當前事務在訪問自己修改過的記錄,該版本可以被當前事務訪問;
2.如果被訪問版本的row trx_id,小於readview中的min_trx_id值,表明生成該版本的事務在當前事務生成readview前已經提交,該版本可以被當前事務訪問;
3.如果被訪問版本的row trx_id,大於或等於readview中的max_trx_id值,表明生成該版本的事務在當前事務生成readview後纔開啓,該版本不可以被當前事務訪問;
4.如果被訪問版本的row trx_id,值在readview的min_trx_id和max_trx_id之間,就需要判斷trx_id屬性值是不是在m_ids列表中,
如果在:說明創建readview時生成該版本的事務還是活躍的,該版本不可以被訪問;
如果不在:說明創建readview時生成該版本的事務已經被提交,該版本可以被訪問。
現有示例如下:
那麼 事務1、事務2、事務4的查詢結果是多少呢?
事務1的兩次查詢結果都是 100
事務2第一次查詢是100,第二次查詢是80
事務4的第一次查詢是90
這個結果是不是同你得出的結果一致呢?現在我們來解析例子中的結果:
start transaction with consistent snapshot; 這條語句執行完就是開啓了一個事務,按照可重複讀的定義,一個事務啓動的時候,能夠看到所有已經提交的事務結果。但是之後,這個事務執行期間,其他事務的更新對它不可見。
假設在事務1之前有一個活躍事務,這個事務id是99;事務1、事務2、事務3、事務4的事務id分別是100,101,102,103;這個例子的期間沒有其他事務;假設這行數據在這三個事務開始之前是row trx_id是90;
那麼:
事務1啓動的時候,read view數組中的值是[99,100]
事務2啓動的時候,read view數組中的值是[99,100,101]
事務3啓動的時候,read view數組中的值是[99,100,101,102]
事務4啓動的時候,事務3已經提交了,所以read view數組中的值是[99,100,101,103]
數據 id = 1 這條記錄的數據版本如下:
從圖中可以看出,第一次有效更新是事務2把(id,score) 從 (1,100)更新成了(1,90),那麼此時這個數據的最新版本就變成了row trx_id 102,而trx_id 90 這個版本就成了歷史版本;第二次有效更新是事務2把(id,score)從當前值(1,90)更新成了(1,80),這個數據的最新版本變成了 row trx_id 101,而trx_id 102成爲了歷史版本。
對於事務1來說,101就是高水位,活躍事務有[99,100],在第一次查詢的時候,id=1的數據的row trx_id是90,小於read view中的最小值即低水位值99,所以此時的數據是可見的;第二次查詢的時候,id=1的數據的row trx_id是101,等於read view中的高水位,數據不可見,繼續往前找數據的歷史版本,發現歷史版本1的row trx_id是102,大於read view中的高水位,數據依然不可見,再往前找歷史版本2的row trx_id是90,小於read view中的最小值即低水位值99,所以此時的數據是可見的;所以事務1兩次的查詢id=1的值都是100
對於事務2來說,102是高水位,活躍事務有[99,100,101],在第一次查詢的時候,id=1的數據的row trx_id是102,等於read view中的高水位,數據不可見,繼續往前找數據的歷史版本2 row trx_id是90,小於read view中的最小值即低水位值99,所以此時的數據是可見的,第一次查詢id=1結果是score=100;第二次查詢的時候,id=1的數據的row trx_id是101,等於自己當前的事務id,就是說現在查詢的數據的更改是自己更改的,所以查詢id=1結果是score=80
事務3對數據進行了修改就提交了,對數據的修改是當前讀。
對於事務4來說,104是高水位,活躍事務有[99,100,101,103],查詢的時候,id=1的數據的row trx_id是101,101介於低水位和高水位之間(介於readview的min_trx_id和max_trx_id之間)且就在活躍的事務列表中,說明此時事務還沒有提交,是不可見的,否則就是髒讀,繼續往前發現歷史版本1的row trx_id是102,102介於低水位和高水位之間,但是不在活躍的事務列表中,說明此時事務已經提交,數據是可見的,所以查詢id=1的結果是90
4.RC(Read Committed,讀提交)隔離級別下的實現
在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的視圖。
在讀提交隔離級別下,"start transaction with consistent snapshot;" 就等於 start transaction,start transaction 命令並不是一個事務的起點,在執行到它們之後的第一個操作 InnoDB 表的語句,事務才真正啓動。
所以示例在RC隔離級別下:
事務1的第一次查詢結果是100,第二次查詢結果是90
事務2第一次查詢是90,第二次查詢是80
事務4的第一次查詢是90