一、InnoDB如何解決幻讀
- 幻讀:在InnoDB的可重復度隔離級別下,使用當前讀,一個事務前后兩次查詢同一個范圍,后一次查詢會看到期間新插入的行;
- 幻讀的影響:會導致一個事務中先產生的鎖,無法鎖住后加入的行,會產生數據一致性問題;
- 產生幻讀的原因:行鎖只能鎖住一行,不能避免新插入的記錄;
- 解決幻讀:在兩行記錄之間加上間隙鎖,阻止新紀錄的插入,與間隙鎖產生沖突的只有“往這個間隙插入記錄”這個操作;
- 同時添加間隙鎖與行鎖稱為Next-key lock,注意間隙鎖只有在InnoDB的可重復度隔離級別下生效;
- MVCC只實現讀取已提交和可重復讀,InnoDB在可重復度的隔離級別下,使用MVCC+Next-key lock解決幻讀;
二、多版本並發控制MVCC
1.基本思想
- MVCC是InnoDB實現隔離級別的一種方式,用於實現讀取已提交和可重復讀兩種隔離級別;
- 對於讀取未提交,直接讀取最新版本的數據;
- 對於串行化,使用加鎖的方式訪問記錄;
- 大多數事務型存儲引擎實現都不是簡單的行鎖,基於並發性的考慮,一般會同時實現多版本並發控制(MVCC)處理讀寫沖突;
- MVCC是樂觀鎖的一種實現,是通過保存數據在某一個時間點的快照實現的,寫操作更新最新的版本,讀操作讀取舊版本;
- MVCC中事務的修改操作(增刪改)會為行記錄新增一個版本快照,並把當前事務id寫入trx_id;
2.版本號
- 系統版本號sys_id:每開始一個新的事務,系統版本號遞增;
- 在InnoDB中,聚簇索引記錄中包含兩個隱藏列:
- trx_id:對記錄進行改動時,trx_id會記錄當前事務id,也就是當前系統版本號;
- roll_pointer:對記錄進行改動,會把舊版本記錄寫入undo日志,roll_pointer指向修改之前的版本;
- 對同一條記錄的更新,會把舊值放到一條undo日志中,作為一個舊版本的記錄,多次更新之后這些版本會被roll_pointer連接成一個鏈表,稱之為版本鏈;
3.版本讀取
- 對於讀取已提交和可重復讀,就會用到版本鏈,關鍵在於怎么判斷版本鏈中哪個版本對當前事務可見;
- 使用ReadView(快照),ReadView是一個包含當前已經開始但是沒有提交的事務的列表,記錄每個事務的事務id,記最小事務id為min_id,最大事務id為max_id;
- 版本比較規則:
- 如果記錄版本的trx_id小於min_id,說明這個記錄版本是已經被提交過的,對其他事務可見;
- 如果記錄版本的trx_id大於max_id,說明這個記錄版本是ReadView生成之后發生的,不能訪問;
- 如果記錄版本的trx_id在min_id和max_id之間,判斷trx_id是否在ReadView中:
- 如果在ReadView中,說明事務還未提交,該記錄版本不可訪問;
- 如果不在ReadView中,說明該事務已經提交,該記錄版本可以訪問;
- 如果當前記錄版本不可讀,就根據回滾指針roll_pointer找到舊版本的記錄再進行判斷;
- 對於讀取已提交,每次查詢都會生成一個新的ReadView;
- 對於可重復度,一個事務在第一次SELECT的時候生成一個ReadView,之后的查詢復用這個ReadView;
4.快照讀與當前讀
- 快照讀:MVCC中的SELECT操作是讀取快照中的數據,不需要進行加鎖;
- 當前讀:MVCC中修改數據的操作(增刪改)需要進行加鎖操作,從而讀取最新的數據;
5.例子
- 假設當前有一個事務id為100的事務A,修改一個記錄的name字段為name2,產生一個版本快照,因此有這樣的版本鏈:
- 假設事務A還沒有提交,此時事務B進行SELECT,事務id為120,查詢id為1的記錄(記為第一次查詢),此時生成ReadView為
[100,120],根據版本讀取規則,先找到trx_id為100的記錄版本,發現不可讀,於是通過回滾指針找到trx_id為60的記錄,讀取成功; - 當事務A提交之后,事務B再次進行SELECT查詢id為1的記錄(第二次查詢),在讀取已提交和可重復讀兩種隔離級別下有不同的情況:
- 如果是讀取已提交,則會創建一個新的ReadView為
[120],此時讀取trx_id為100的記錄成功,也就是讀取到了在事務期間提交的數據; - 如果是可重復讀,則會使用第一次查詢時的ReadView為
[100,120],此時讀取的是trx_id為60的記錄,從而實現了可重復度;
菜雞的小疑惑:
三級封鎖協議不是可以解決臟讀和不可重復度的問題嗎,為什么要用MVCC來實現,是性能更好嗎?
轉自:https://zhuanlan.zhihu.com/p/180350695
