MySQL MVVC多版本並發控制的實現詳解
一、概述
MVCC(Multiversion Concurrency Control),多版本並發控制。它和undo log中的版本鏈息息相關,MVVC通過數據行的多個版本來實現數據庫的並發控制。
簡單的說就是當前事務查詢另一個事務正在更改的行(如果此時讀取就會發生臟讀),不用加鎖等待,而是讀取該數據的歷史版本,降低響應時間。
MVVC是通過undo log和Read View兩種技術實現的。
二、快照讀與當前讀
MVCC在MySQL InnoDB中的實現主要是為瞭提高數據庫並發性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞並發讀 ,而這個讀指的就是快照讀 , 而非當前讀。當前讀實際上是一種加鎖的操作。
1.當前讀
當前讀讀取的記錄一定是最新的數據,讀取時還要保證其他並發事務不能修改當前記錄,會對讀取的記錄進行加鎖。
加鎖的讀被稱為當前讀,還有數據的增刪改都是要先讀取數據的,這一讀取過程也是當前讀。
SELECT * FROM t LOCK IN SHARE MODE; # 共享鎖 SELECT * FROM t FOR UPDATE; # 排他鎖 UPDATE SET t..
2.快照讀
快照讀又叫一致性讀,讀取的是數據行的快照版本。在MySQL中,普通的select語句(不加for update或lock in share mode的select語句)默認就是使用的快照讀,不加鎖。
SELECT * FROM table WHERE ...
之所以這樣,是因為快照讀可以避免加鎖操作,降低開銷。
當事務的隔離級別是串行時,快照讀就沒有用瞭,會退化為當前讀。
三、隔離級別與版本鏈復習
隔離級別:
在MySQL中默認的隔離級別就是可重復讀RR,可以解決不可重復讀問題,在MySQL中,特別的還額外支持解決幻讀問題。
它是如何解決幻讀問題的呢?有兩種方式:
- 使用間隙鎖和臨鍵鎖解決,簡而言之就是加鎖,在此期間其他事務不能夠插入數據
- MVCC方式,無需加鎖,消耗低(缺點是沒有完全解決幻讀問題)。
undo log版本鏈:
對應InnoDB來說,聚簇索引中的每個記錄都包含瞭兩個必要的隱藏字段:
- trx_id:每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給trx_id隱藏列。
- roll_pointer:回滾指針,每次修改數據時,都會把舊數據放入undo log日志中,新的數據指向該舊數據,做成一個版本鏈,該指針字段就稱為回滾指針,通過該指針可以找到修改前的數據。
舉例:
有一個id為8的事務創建瞭一條數據,那麼該記錄的示意圖大概如下:
假設之後兩個id分別為10、20的事務對這條記錄進行update操作,流程如下:
事務10 | 事務20 |
---|---|
BEGIN; | |
BEGIN; | |
UPDATE student SET name='李四' WHERE id=1; | |
UPDATE student SET name='王五' WHERE id=1; | |
COMMIT; | |
UPDATE student SET name='趙六' WHERE id=1; | |
UPDATE student SET name='錢七' WHERE id=1; | |
COMMIT; |
每次修改都會生成一個undo log日志,每個日志都相互鏈接,構成版本鏈,此時該條數據的示意圖如下:
每個版本中還包含生成該版本時對應的事務id 。
四、Read View
有瞭undo log就可以讀取到記錄的歷史版本,那麼在什麼情況下,讀取哪個版本的記錄呢?這就用到瞭Read View,它幫我們解決瞭行的可見性問題。
Read View就是當某個事務在使用MVVC機制進行快照讀操作時產生的讀視圖。該視圖是數據庫當前所有活躍事務id(還未提交的事務)組成的列表的一個快照。
1.實現原理
四種隔離級別裡,讀未提交和串行化是不會使用MVVC的,因為讀未提交直接讀取某個數據的最新數據即可,串行化是通過加鎖來讀的。
讀已提交和可重復讀都必須保證讀到的數據都是其他事務提交瞭的,所以,其他事務修改瞭數據但是還未提交,我們不能夠訪問該數據,但可以通過MVVC機制讀取該記錄的歷史版本,核心問題就是需要判斷版本鏈中的哪條歷史版本是當前事務可見的,這也是ReadView要解決的問題。
Read View包含4個比較重要的內容:
- creator_trx_id:創建這個Read View的事務id,Read View和事務是一一對應的。
隻有事務對表中的記錄做修改時才會為事務分配事務id,否則一個事務中隻有讀操作,該事務的id默認為0。
- trx_ids:表示在生成Read View時當前系統中活躍的事務id列表。提交瞭的事務不在其中。
- up_limit_id:活躍的事務中最小的事務id。
- low_limit_id:表示生成Read View時系統應該分配給下一個事務的id值,同樣也表示系統中最大的事務id值。
註意:low_limit_id並不是trx_ids中的最大值,事務id是遞增分配的。比如,現在有id為1, 2,5這三個事務,之後id為5的事務提交瞭。那麼一個新的讀事務在生成ReadView時, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是6。
2.Read View規則
版本鏈
當某個事務有瞭Read View,訪問某條記錄時,需要按照下面的步驟判斷該記錄的哪個版本可見:
- 如果該版本記錄的trx_id和Read View的creator_trx_id相同,意味著該版本的記錄是由當前事務修改的,因此該版本可以被當前事務訪問
- 如果該版本記錄的trx_id小於Read View的up_limit_id,證明當前事務生成Read View時,此事務已經提交瞭,所以當前事務可以讀取該版本。
- 如果該版本的trx_id大於等於low_limit_id,證明生成該版本的事務在當前事務生成Read View之後才開啟,所以該版本不可以被當前事務訪問。
- 如果被訪問版本的trx_id屬性值在ReadView的up_limit_id和low_limit_id之間,那就需要判斷一下trx_id屬性值是不是在trx_ids列表中,如果不在的話才能訪問,否則不能訪問。
3.整體流程
瞭解瞭這些概念之後,我們來看下當查詢一條記錄的時候,系統如何通過MVCC找到它:
- 首先獲取事務自己的版本號,也就是事務ID;
- 獲取 ReadView;
- 查詢得到的數據,然後與 ReadView 中的事務版本號進行比較;
- 如果不符合 ReadView 規則,就需要從Undo Log中獲取歷史快照;
- 最後返回符合規則的數據。
在隔離級別為讀已提交時,一個事務中的每一次SELECT查詢都會重新獲取一次Read View,而可重復讀是第一SELECT操作才會生成Read View,之後的查詢操作復用這一個。
導致這兩種的差距是因為:可重復讀要保證一個事務中相同的SELECT讀取的內容是相同的。
五、舉例
1.READ
COMMITTED隔離級別下
現在有兩個事務id分別為10、20的事務在執行:
-- id為10的事務 begin; update t set name='李四' where id=1; update t set name='王五' where id=1; -- id為20的事務 更新其他行的數據
此刻,表中id為1的記錄得到的版本鏈表如下所示:
此時新來一個事務執行如下操作:
begin; select * from t where id=1; -- 事務10、20未提交
查詢到的結果為張三。
具體的過程如下:
- 在執行select語句前,先生成一個Read View,Read View的creator_trx_id為0,trx_ids列表的內容是[10,20],up_limit_id為10,low_limit_id為21。
- 查詢name為王五的最新版本的記錄,按規則進行對比,因為trx_id為10,10剛好是trx_ids中的記錄,所以這條記錄對當前事務不可見,根據回滾指針得到下一個版本
- 下一個版本name為李四,也不行
- 繼續找到name為張三的版本,trx_id為8,8小於up_limit_id,所以該版本對當前事務可見,得到最終結果
接下來,再將id為10的事務進行commit提交。然後id為20的事務來更新記錄:
begin; -- id為20的事務 update t set name='趙六' where id=1; update t set name='錢七' where id=1;
此時版本鏈更新為:
再到剛才使用READ COMMITTED隔離級別的事務中繼續查找這個id 為1的記錄,得到的結果為name=王五的那條記錄。執行過程如下:
- 生成Read View,Read View的creator_trx_id為0,trx_ids列表的內容是[20],up_limit_id為20,low_limit_id為21。
- 因為前兩個版本的記錄trx_id為20,存在trx_ids中,所以跳過
- 到第三條記錄時,trx_id為10,小於20,可以讀取,所以最終結果為王五
註意:READ COMMITTED,每次讀取數據前都生成一個新的ReadView。
2.REPEATABLE READ隔離級別下
假如此時id為10的事務和id為20的事務正在修改,都未提交,修改內容和前面的一樣,但是還未提交,此時當前事務做一個查詢。
步驟為:
- 生成Read View,Read View的creator_trx_id為0,trx_ids列表的內容是[10,20],up_limit_id為10,low_limit_id為21。
- trx_id為10和20的都不滿足要求
- 最後查找到name為張三的歷史版本的數據
此時,id為10的記錄提交事務。
當前事務又需要select id為1的記錄,步驟為:
- 因為是可重復讀,且第一次select已經生成過Read View瞭,所有會復用它,不重新生成。
- 所以trx_id為10和20的記錄依舊不符合規則,最終得到的數據還是張三,符合可重復讀的規范
註意:REPEATABLE READ,每次讀取都復用第一次生成的Read View
3.如何解決幻讀
假設現在有一條數據,id為1
當前活躍的事務有10和20。
此時當前事務啟動瞭,執行如下SQL語句:
begin; select * from student where id>=1;
在開始前生成Read View,內容如下:creator_trx_id=0,trx_ids= [10,20] , up_limit_id=10, low_limit_id=21。
由於id大於等於1的數據隻有一個,且該數據的trx_id為8,小於up_limit_id,所以可以讀取到。
在這之後id為10的事務新增瞭一行數據,增加瞭id為2的數據,且提交瞭。
此時當前線程繼續查找id>=1的數據,因為是可重復讀,復用剛剛的Read View。
得到兩行數據,但是因為id為2的數據trx_id為10,該值在Read View的trx_ids中存在,所以該記錄對當前事務不可見,所以最後查詢到的數據隻有一條記錄。
如果當前事務再插入id為2的數據就插不進去,所以說MVVC隻解決瞭一半的幻讀問題。
到此這篇關於MySQL MVVC多版本並發控制的實現詳解的文章就介紹到這瞭,更多相關MySQL MVVC內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- MySQL多版本並發控制MVCC詳解
- 一文解析MySQL的MVCC實現原理
- MySQL多版本並發控制MVCC底層原理解析
- MySQL事務的隔離性是如何實現的
- Mysql隔離性之Read View的用法說明