14、MVCC

1、事務併發執行碰到的問題

參考:
https://time.geekbang.org/column/article/68963
https://juejin.im/book/6844733769996304392/section/6844733770071801870
https://time.geekbang.org/column/article/69236
1) 髒寫: 如果一個事務修改了另一個未提交事務修改過的數據, 那就意味着發生了髒寫. 
Session A和Session B各開啓了一個事務,Session B中的事務先將number列爲1的記錄的name列更新爲'關羽',然後Session A
中的事務接着又把這條number列爲1的記錄的name列更新爲張飛.如果之後Session B中的事務進行了回滾,那麼Session A中的更
新也將不復存在,這種現象就稱之爲髒寫.這時Session A中的事務就很懵逼,我明明把數據更新了,最後也提交事務了,怎麼到最後說
自己啥也沒幹呢?

2) 髒讀: 如果一個事務讀到了另一個未提交事務修改過的數據, 那就意味着發生了髒讀.
如上圖,Session A和Session B各開啓了一個事務,Session B中的事務先將number列爲1的記錄的name列更新爲'關羽',然後
Session A中的事務再去查詢這條number爲1的記錄,如果讀到列name的值爲'關羽',而Session B中的事務稍後進行了回滾,那麼
Session A中的事務相當於讀到了一個不存在的數據,這種現象就稱之爲髒讀.

3) 不可重複讀: 如果一個事務只能讀到另一個已經提交的事務修改過的數據, 並且其他事務每對該數據進行一次修改並提交後,該
事務都能查詢得到最新值, 那就意味着發生了不可重複讀.我們在Session B中提交了幾個隱式事務(注意是隱式事務,意味着語句結
束事務就提交了),這些事務都修改了number列爲1的記錄的列name的值,每次事務提交之後,如果Session A中的事務都可以查看到
最新的值,這種現象也被稱之爲不可重複讀.
你可能會問那什麼時候需要“可重複讀”的場景呢?
我們來看一個數據校對邏輯的案例.假設你在管理一個個人銀行賬戶表.一個表存了賬戶餘額,一個表存了賬單明細.到了月底你要
做數據校對,也就是判斷上個月的餘額和當前餘額的差額,是否與本月的賬單明細一致.你一定希望在校對過程中,即使有用戶發生
了一筆新的交易,也不影響你的校對結果.

4) 幻讀: 如果一個事務先根據某些條件查詢出一些記錄, 之後另一個事務又向表中插入了符合這些條件的記錄, 原先的事務再次按
照該條件查詢時, 能把另一個事務插入的記錄也讀出來, 那就意味着發生了幻讀.如圖,Session A中的事務先根據條件number > 0
這個條件查詢表hero,得到了name列值爲'劉備'的記錄;之後Session B中提交了一個隱式事務,該事務向表hero中插入了一條新記
錄;之後Session A中的事務再根據相同的條件number > 0查詢表hero,得到的結果集中包含Session B中的事務新插入的那條記
錄,這種現象也被稱之爲幻讀.有的同學會有疑問,那如果Session B中是刪除了一些符合number > 0的記錄而不是插入新記錄,那
Session A中之後再根據number > 0的條件讀取的記錄變少了,這種現象算不算幻讀呢?明確說一下,這種現象不屬於幻讀,幻讀強
調的是一個事務按照某個相同條件多次讀取記錄時,後讀取時讀到了之前沒有讀到的記錄.

2、MVCC

2.1、版本鏈

InnoDB存儲引擎, 其聚簇索引記錄中包含兩個必要的隱藏列
1) trx_id: 每次一個事務對某條聚簇索引記錄進行改動時, 都會把該事務的事務id賦值給trx_id隱藏列
2) roll_pointer: 每次對某條聚簇索引記錄進行改動時, 都會把舊的版本寫入到undo日誌中, 然後這個隱藏列就相當於一個
指針, 可以通過它來找到該記錄修改前的信息
假設插入一條記錄, 那麼此刻該記錄的示意圖如下

假設之後兩個事務id分別爲100、200的事務對這條記錄進行UPDATE操作, 操作流程如下

每次對記錄進行改動, 都會記錄一條undo日誌, 每條undo日誌也都有一個roll_pointer屬性(INSERT操作對應的undo日誌沒有該屬
性, 因爲該記錄並沒有更早的版本), 可以將這些undo日誌都連起來, 串成一個鏈表, 所以現在的情況就像下圖一樣

對該記錄每次更新後,都會將舊值放到一條undo日誌中,就算是該記錄的一箇舊版本,隨着更新次數的增多,所有的版本都會被
roll_pointer屬性連接成一個鏈表,我們把這個鏈表稱之爲版本鏈,版本鏈的頭節點就是當前記錄最新的值.另外,每個版本中還包含
生成該版本時對應的事務id,這個信息很重要,我們稍後就會用到.

2.2、ReadView

對於使用READ UNCOMMITTED隔離級別的事務來說,由於可以讀到未提交事務修改過的記錄,所以直接讀取記錄的最新版本就好
了. 對於使用SERIALIZABLE隔離級別的事務來說,設計InnoDB的大叔規定使用加鎖的方式來訪問記錄. 對於使用READ COMMITTED
和REPEATABLE READ隔離級別的事務來說,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另一個事務已經修改
了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是:需要判斷一下版本鏈中的哪個版本是當前事務可見的.爲
此,設計InnoDB的大叔提出了一個ReadView的概念,這個ReadView中主要包含4個比較重要的內容:
1) m_ids: 表示在生成ReadView時當前系統中活躍的讀寫事務的事務id列表
2) min_trx_id: 表示在生成ReadView時當前系統中活躍的讀寫事務中最小的事務id, 也就是m_ids中的最小值
3) max_trx_id: 表示生成ReadView時系統中應該分配給下一個事務的id值
4) creator_trx_id: 表示生成該ReadView的事務的事務id
有了這個ReadView,這樣在訪問某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:

1) 如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味着當前事務在訪問它自己修改過的記錄,
   所以該版本可以被當前事務訪問.

2) 如果被訪問版本的trx_id屬性值小於ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前
   已經提交, 所以該版本可以被當前事務訪問.

3) 如果被訪問版本的trx_id屬性值大於或等於ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView
   後纔開啓,所以該版本不可以被當前事務訪問.

4) 如果被訪問版本的trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下trx_id屬性值是不是
   在m_ids列表中,如果在,說明創建ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問. 如果不在,說明創建
   ReadView時生成該版本的事務已經被提交,該版本可以被訪問.

5) 如果某個版本的數據對當前事務不可見的話,那就順着版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類
   推,直到版本鏈中的最後一個版本.如果最後一個版本也不可見的話,那麼就意味着該條記錄對該事務完全不可見,查詢結果就不
   包含該記錄
READ COMMITTD、REPEATABLE READ這兩個隔離級別的一個很大不同就是:生成ReadView的時機不同,READ COMMITTD在每
一次進行普通SELECT操作前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操作前生成一個ReadView,
之後的查詢操作都重複使用這個ReadView就好了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章