閱讀本文大約需要 20 分鐘。概念多一點, 請耐心讀完。
之前學習了MySQL事務隔離級別,在可重複讀的隔離級別下,如果一個事務啟動的時候,會創建一個視圖 read view,之後這個事務在執行期間,即使有其他事務修改了數據,這個事務看到的仍然和啟動時是一樣的,好像別人做的任何操作和他無關一樣。普通視圖,view。它是一個查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果。
語法:CREATE VIEW 視圖名 AS SELECT 查詢語句。
InnoDB在實現MVCC時用到的一致性讀視圖,consistent read view。用於支持RC(read committed, 讀提交) 和 RR(repeatable read, 可重複讀)隔離級別的實現。
在之前的事務隔離中筆記中,我們知道,視圖的概念只存在於可重複讀和讀提交這兩個隔離級別中。在可重複讀的級別下,事務在啟動的時候,就「拍了快照」,而且這個快照是基於整個庫的。
(一行記錄狀態變更 示意圖)
m_ids:表示在生成read-view時,也就是這個事務啟動瞬間,當前正在「活躍」的所有事務ID 的數組。活躍 -- 啟動了但還沒提交的事務。
min_trx_id:表示在生成read-view時當前系統中「活躍」的事務ID中最小的事務ID,也就是m_ids中最小值。
max_trx_id:表示生成read-view時系統中應該分配給下一個事務的id值
tips:注意max_trx_id並不是m_ids中的最大值,事務id是遞增分配的。比方說現在有id為1,2,3這三個事務,之後id為3的事務提交了。那麼一個新的讀事務在生成read-view時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
creator_trx_id:表示在生成read-view 事務的事務ID。就是當前操作生成的事務 的 事務ID。
tips:只有對表中的數據記錄做改動時(執行insert, delete, update這些語句時)才會生成事務id,否則在一個只讀事務中的事務id值默認都為0。
我們把事務ID中的min_trx_id記為低水位,max_trx_id即為高水位。這個視圖數組和高水位就組成了當前事務的一致性視圖。而數據版本的可見性規則,就是基於數據的 row trx_id 和 這個一致性視圖的對比結果得到的。如果被訪問版本的 trx_id 屬性值 與 read-view 中 createor_trx_id 值相同,意味著當前事務在訪問自己修改過的記錄,所以該版本是可見的。
如果被訪問版本的 trx_id 屬性值 小於 read-view 中 min_trx_id 的值,意味著生成該版本的事務在當前事務生成 read-view 之前已經提交,所以該版本是可見的。
如果被訪問版本的 trx_id 屬性值 大於或等於 read-view 中 max_trx_id 的值,意味著生成該版本的事務在當前事務生成 read-view 之後才開啟(生成的),所以該版本是不可見的。
如果被訪問的版本 trx_id 屬性值在 read-view 的 min_trx_id 和 max_trx_id 之間,這需要判斷 trx_id 屬性值是不是在 m_ids 數組中。如果存在,說明創建生成 read-view 時生成該版本的事務還是活躍的,該版本是不可見的。如果不存在,說明創建生成 read-view 時生成該版本的事務已經被提交了,所以該版本是可見的。
(數據版本可見性規則 示意圖)
如果落在綠色部分,表示這個版本是已提交的事務或者是當前事務自己生成的,這個數據是可見的;
如果落在紅色部分,表示這個版本是由將來啟動的事務生成的,是肯定不可見的;
如果落在黃色部分,那就包括兩種情況
a. 若 row trx_id 在數組中,表示這個版本是活躍的還沒提交的事務生成的,不可見;
b. 若 row trx_id 不在數組中,表示這個版本是已經提交了的事務生成的,可見。
比方說現在系統裡有一張hero表,假設現在表中只有一個事務id為80的事務插入一條記錄。mysql> SELECT * FROM hero;+---+---+----+| number | name | country |+---+---+----+| 1 | 劉備 | 蜀 |+---+---+----+接下來看一下READ COMMITTED和REPEATABLE READ所謂的生成read-view的時機不同到底不同在哪裡。讀提交 -- 每次讀取數據前都生成一個 read-view
比方說現在系統裡有兩個事物id,分別是100,200的事務在進行:# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;# Transaction 200BEGIN;# 更新了一些別的表的記錄...tips: 事務執行過程中,只有在第一次真正修改記錄時(比如使用INSERT、DELETE、UPDATE語句),才會被分配一個單獨的事務id,這個事務id是遞增的。在Transaction 200中更新一些別的表的記錄,目的是讓它分配事務id。
(hero 表中 number 為1的記錄 版本鍊表 示意圖)
# 使用READ COMMITTED隔離級別的事務BEGIN;# SELECT1:Transaction 100、200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'在執行SELECT語句時會先生成一個read-view,read-view的m_ids列表的內容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。
然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'張飛',該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。
下一個版本的列name的內容是'關羽',該版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。
下一個版本的列name的內容是'劉備',該版本的trx_id值為80,小於read-view中的min_trx_id值100,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列name為'劉備'的記錄。
# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;COMMIT;然後再到事務id為200的事務中更新一下表hero中number為1的記錄:# Transaction 200BEGIN;# 更新了一些別的表的記錄...UPDATE hero SET name = '趙雲' WHERE number = 1;UPDATE hero SET name = '諸葛亮' WHERE number = 1;(hero 表中 number 為1的記錄 版本鍊表 示意圖)然後再到剛才使用READ COMMITTED隔離級別的事務中繼續查找這個number為1的記錄,如下:# 使用READ COMMITTED隔離級別的事務BEGIN;# SELECT1:Transaction 100、200均未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'# SELECT2:Transaction 100提交,Transaction 200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'張飛'• 在執行SELECT語句時會又會單獨生成一個read-view,該read-view的m_ids列表的內容就是[200](事務id為100的那個事務已經提交了,所以再次生成快照時就沒有它了),min_trx_id為200,max_trx_id為201,creator_trx_id為0。• 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。• 下一個版本的列name的內容是'趙雲',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。• 下一個版本的列name的內容是'張飛',該版本的trx_id值為100,小於read-view中的min_trx_id值200,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列name為'張飛'的記錄。以此類推,如果之後事務id為200的記錄也提交了,再次在使用READ COMMITTED隔離級別的事務中查詢表hero中number值為1的記錄時,得到的結果就是'諸葛亮'了。總結一下就是:使用READ COMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的read-view可重複讀 -- 在第一次讀取數據時生成一個read-view
使用REPEATABLE READ隔離級別的事務來說,只會在第一次執行查詢語句時生成一個read-view,之後的查詢就不會重複生成了。舉例:
比方說現在系統裡有兩個事務id分別為100、200的事務在執行:# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;# Transaction 200BEGIN;# 更新了一些別的表的記錄...(hero 表中 number 為1的記錄 版本鍊表 示意圖)
假設現在有一個使用REPEATABLE READ隔離級別的事務開始執行:# 使用REPEATABLE READ隔離級別的事務BEGIN;# SELECT1:Transaction 100、200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'SELECT1的執行過程如下:
• 在執行SELECT語句時會先生成一個read-view,read-view的m_ids列表的內容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。
• 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'張飛',該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。
• 下一個版本的列name的內容是'關羽',該版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。
• 下一個版本的列name的內容是'劉備',該版本的trx_id值為80,小於read-view中的min_trx_id值100,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列name為'劉備'的記錄。
把事務id 為 100 的事務提交一下:
# Transaction 100BEGIN;UPDATE hero SET name = '關羽' WHERE number = 1;UPDATE hero SET name = '張飛' WHERE number = 1;COMMIT;然後再到事務id為200的事務中更新一下表hero中number為1的記錄:
# Transaction 200BEGIN;# 更新了一些別的表的記錄...UPDATE hero SET name = '趙雲' WHERE number = 1;UPDATE hero SET name = '諸葛亮' WHERE number = 1;(hero 表中 number 為1的記錄 版本鍊表 示意圖)
然後再到剛才使用REPEATABLE READ隔離級別的事務中繼續查找這個number為1的記錄
# 使用REPEATABLE READ隔離級別的事務BEGIN;# SELECT1:Transaction 100、200均未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值為'劉備'# SELECT2:Transaction 100提交,Transaction 200未提交SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍為'劉備'• 因為當前事務的隔離級別為REPEATABLE READ,而之前在執行SELECT1時已經生成過read-view了,所以此時直接復用之前的read-view,之前的read-view的m_ids列表的內容就是[100, 200],min_trx_id為100,max_trx_id為201,creator_trx_id為0。• 然後從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列name的內容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。• 下一個版本的列name的內容是'趙雲',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。• 下一個版本的列name的內容是'張飛',該版本的trx_id值為100,而m_ids列表中是包含值為100的事務id的,所以該版本也不符合要求,同理下一個列name的內容是'關羽'的版本也不符合要求。繼續跳到下一個版本。• 下一個版本的列name的內容是'劉備',該版本的trx_id值為80,小於read-view中的min_trx_id值100,所以這個版本是符合要求的,最後返回給用戶的版本就是這條列c為'劉備'的記錄。也就是說兩次SELECT查詢得到的結果是重複的,記錄的列c值都是'劉備',這就是可重複讀的含義。事務 A 開始前,系統裡面只有一個活躍事務 ID 是 99;
事務 A、B、C 的版本號分別是 100、101、102,且當前系統裡只有這四個事務;
三個事務開始前,(1,1)這一行數據的 row trx_id 是 90。
事務 A 的視圖數組就是[99,100], 事務 B 的視圖數組是[99,100,101], 事務 C 的視圖數組是[99,100,101,102]。事務C先把數據(1,1)改為(1,2), 這個數據最新版本的row trx_id 是 102, 90這個版本已經變為歷史版本.
事務B把數據從 (1,2) 改成了 (1,3)。這時候,這個數據的最新版本(即 row trx_id)是 101,而 102 又成為了歷史版本。在這裡我有一個疑問, 就是如果按照一致性讀的話, 這裡好像是不對的。事務B 事務數組是先生成的,之後C才提交,不應該看見(1,2)的,為什麼結果是(1,3)?如果在事務B在更新之前查詢一次數據,這個查詢返回的結果確實是1。但是,當他要去更新數據的時候,就不能再歷史版本上更新了,否則事務C的更新就丟失了。
這裡會用到一個規則,更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱之為當前讀。觸發當前讀除了 update 語句外,select 語句如果加鎖,也是當前讀。
所以,在執行事務 B 查詢語句的時候,一看自己的版本號是 101,最新數據的版本號也是 101,是自己的更新,可以直接使用,所以查詢得到的 k 的值是 3。元宵節了
祝大家
所求皆所願
所盼皆所期
燈火璀璨
萬家平安
最後,求關注。每天進步一點點,歡迎關注我的公眾號「白砂」。
如果我的文章對你有所幫助,還請幫忙點讚、在看、轉發一下,非常感謝!