Mysql MVCC原理和幻讀解決
reference:https://blog.csdn.net/weixin_43477531/article/details/121963884
reference:https://www.cnblogs.com/xuwc/p/13873293.html
1、MVCC全稱(Multi-Version Concurrency Control),即多版本並發控制,主要是為了提高數據庫的並發性能,解決幻讀問題。
2、快照讀、當前讀
快照讀:顧名思義,就是讀取的是快照數據,不加鎖的普通select都是快照讀
當前讀:就是讀取最新數據,而不是歷史數據,或者說不是快照數據,是加鎖的select,或者對數據進行正刪改都會進行當前讀。
3、MVCC解決問題:
- 並發讀-寫場景:可以做到讀操作不阻塞寫操作,同時寫操作也不會阻塞讀操作。
- 解決臟讀、幻讀、不可重復讀等事務隔離問題。但不能解決並發寫-寫場景問題。
4、MVCC原理
實現原理主要是版本鏈。undo日志、ReadView來實現的。
InnoDB 存儲引擎,表中的聚簇索引都包含三個隱藏列(row_id、trx_id、roll_pointer)。
row_id:創建的表中有主鍵或者非NULL的unique鍵時都不會包含row_id列。
trx_id:事務ID,每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給trx_id隱藏列。
roll_pointer:回滾指針,每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到undo日志中,然后這個隱藏列就相當於一個指針,可以通過它來找到該記錄修改前的信息。
版本鏈:每次對記錄進行改動,都會記錄一條 undo 日志,每條 undo 日志也都有一個 roll_pointer 屬性(INSERT 操作對應的 undo 日志沒有該屬性,因為該記錄並沒有更早的版本),可以將這些 undo 日志都連起來,串成一個鏈表。版本鏈的頭節點就是當前記錄最新的值。
5、undo日志
undo log 主要用於記錄數據被修改之前
的日志,在表信息修改之前先會把數據拷貝到undo log
里。當事務
進行回滾時
可以通過 undo log 里的日志進行數據還原
。
用途:
- 保證事務進行rollback時的原子性和一致性,當事務進行回滾的時候可以用undo日志的數據進行恢復。
- 用於MVCC
快照讀
的數據。在MVCC多版本控制中,通過讀取undo日志的歷史版本數據可以實現不同事務版本號都擁有自己獨立的快照數據版本。
類別:
- insert undo log
代表事務在insert新記錄時產生的undo log , 只在事務回滾時需要,並且在事務提交后可以被立即丟棄。
- update undo log
事務在進行 update 或 delete 時產生的 undo log, 不僅在事務回滾時需要,在快照讀時也需要。所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日志時,對應的日志才會被 purge 線程統一清除。
6、ReadView
改動的記錄都存在在 undo 日志中,那如果一個日志需要查詢行記錄,需要讀取哪個版本的行記錄呢?
-
對於使用 READ UNCOMMITTED 隔離級別的事務來說,由於可以讀到未提交事務修改過的記錄,所以直接讀取記錄的最新版本就好了。
-
對於使用
SERIALIZABLE
隔離級別的事務來說,InnoDB 使用加鎖的方式來訪問記錄,不存在並發問題。 -
而對於使用
READ COMMITTED
和REPEATABLE READ
隔離級別的事務來說,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的。
核心問題就是: READ COMMITTED
和 REPEATABLE READ
隔離級別在不可重復讀和幻讀上的區別在哪里?這兩種隔離級別對應的不可重復讀與幻讀問題都是指同一個事務在兩次讀取記錄時出現不一致的情況,這兩種隔離級別關鍵是需要判斷版本鏈中的哪個版本是當前事務可見的。
ReadView 就是用來解決這個問題的,可以幫助我們解決可見性問題。 事務進行快照讀操作的時候就會產生 Read View,它保存了當前事務開啟時所有活躍的事務列表(這里的活躍指的是未提交的事務。)
每一個事務在啟動時,都會生成一個 ReadView,用來記錄一些內容,ReadView 中主要包含 4 個比較重要的屬性:
m_ids:事務id列表,生成 ReadView 時當前系統中活躍的讀寫事務的事務 id 列表。
min_trx_id:最小事務id,生成 ReadView 時當前系統中活躍的讀寫事務中最小的事務 id 也就是 m_ids 中的最小值。
max_trx_id:下一個事務id,生成 ReadView 時系統中應該分配給下一個事務的 id 值。
creator_trx_id:當前ReadView所屬事務id,生成該 ReadView 的事務的事務 id,指定當前的 ReadView 屬於哪個事務。
其中,max_trx_id並不是指m_ids中的最大值,因為事務 id 是遞增分配的,假如現在有 id 為 1,2,3 這三個事務,之后 id 為 3 的事務提交了。那么一個新的讀事務在生成 ReadView 時,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。
再有了 ReadView 之后,在訪問某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:
-
trx_id = creator_trx_id
,可訪問意味着當前事務在訪問它自己修改過的記錄,所以該版本可以被當前事務訪問。
-
trx_id < min_trx_id
,可訪問表明生成該版本的事務在當前事務生成 ReadView 前已經提交,所以該版本可以被當前事務訪問。
-
trx_id >= max_trx_id
,不可訪問表明生成該版本的事務在當前事務生成 ReadView 后才開啟,所以該版本不可以被當前事務訪問。
-
min_trx_id <= trx_id < max_trx_id
,存在m_ids
列表中不可訪問如果在,說明創建 ReadView 時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明創建 ReadView 時生成該版本的事務已經被提交,該版本可以被訪問。
-
某個版本的數據對當前事務不可見
如果某個版本的數據對當前事務不可見的話,那就順着版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個版本。如果最后一個版本也不可見的話,那么就意味着該條記錄對該事務完全不可見,查詢結果就不包含該記錄。
在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔離級別的的一個非常大的區別就是它們生成 ReadView 的時機不同。
7、MVCC下的幻讀
幻讀是一個事務按照某個相同條件多次讀取記錄時,后讀取時讀到了之前沒有讀到的記錄,而這個記錄來自另一個事務添加的新記錄,也就是說幻讀是指新插入的行。
在 REPEATABLE READ 隔離級別下,事務 A 第一次執行普通的 SELECT 語句時生成了一個 ReadView(且在 RR 下只會生成一個 RV),之后事務 B 向 user 表中新插入一條記錄並提交。
ReadView 並不能阻止事務 A 執行 UPDATE 或者 DELETE 語句來改動這個新插入的記錄(由於事務 B 已經提交,因此改動該記錄並不會造成阻塞),但是這樣一來,這條新記錄的 trx_id 隱藏列的值就變成了事務 A 的事務 id。之后 A 再使用普通的 SELECT 語句去查詢這條記錄時就可以看到這條記錄了,也就可以把這條記錄返回給客戶端。
因為這個特殊現象的存在,我們也可以認為 MVCC 並不能完全禁止幻讀。
8、解決幻讀問題
我們知道數據庫的讀操作分為當前讀和快照讀,而在 RR 隔離級別下,MVCC 解決了在快照讀的情況下的幻讀,而在實際場景中,我們可能需要讀取實時的數據,比如在銀行業務等特殊場景下,必須是需要讀取到實時的數據,此時就不能快照讀。
毫無疑問,在並發場景下,我們可以通過加鎖的方式來實現當前讀,而在 MySQL 中則是通過Next-Key Locks
來解決幻讀的問題。(關於 MySQL 中的鎖的介紹可以看看這篇文章:一文了解 MySQL 中的鎖)。
Next-Key Locks
包含兩部分:記錄鎖(行鎖,Record Lock),間隙鎖(Gap Locks)。記錄鎖是加在索引上的鎖,間隙鎖是加在索引之間的。
- Record Lock
記錄鎖,單條索引記錄上加鎖。
Record Lock 鎖住的永遠是索引,不包括記錄本身,即使該表上沒有任何索引,那么innodb會在后台創建一個隱藏的聚集主鍵索引,那么鎖住的就是這個隱藏的聚集主鍵索引。
記錄鎖是有 S 鎖和 X 鎖之分的,當一個事務獲取了一條記錄的 S 型記錄鎖后,其他事務也可以繼續獲取該記錄的 S 型記錄鎖,但不可以繼續獲取 X 型記錄鎖;當一個事務獲取了一條記錄的 X 型記錄鎖后,其他事務既不可以繼續獲取該記錄的 S 型記錄鎖,也不可以繼續獲取 X 型記錄鎖。
- Gap Locks
間隙鎖,對索引前后的間隙上鎖,不對索引本身上鎖。前開后開區間。
MySQL 在 REPEATABLE READ 隔離級別下是可以解決幻讀問題的,解決方案有兩種。
-
可以使用 MVCC 方案解決
-
也可以采用加鎖方案解決(間隙鎖)。
但是在使用加鎖方案解決時有問題,就是事務在第一次執行讀取操作時,那些幻影記錄尚不存在,我們無法給這些幻影記錄加上記錄鎖。所以我們可以使用間隙鎖對其上鎖。
索引對間隙鎖會產生什么影響?
-
對主鍵或唯一索引,如果當前讀時,where 條件全部精確命中(=或in),這種場景本身就不會出現幻讀,所以只會加行鎖,也就是說間隙鎖會退化為行鎖(記錄鎖)。
-
非唯一索引列,如果 where 條件部分命中(>、<、like等)或者全未命中,則會加附近間隙鎖。例如,某表數據如下,非唯一索引2,6,9,9,11,15。如下語句要操作非唯一索引列 9 的數據,間隙鎖將會鎖定的列是(6,11],該區間內無法插入數據。
-
對於沒有索引的列,當前讀操作時,會加全表間隙鎖,生產環境要注意。
Next-Key Locks
next-key locks 是索引記錄上的行鎖和索引記錄之前的間隙鎖的組合,包括記錄本身,每個 next-key locks 是前開后閉區間(同樣說明鎖住的范圍更大,影響並發度),也就是說間隙鎖只是鎖的間隙,沒有鎖住記錄行,next-key locks 就是間隙鎖基礎上鎖住右邊界行數據。
對於可重復讀默認使用的就是next key lock,但是對於“唯一索引”,比如主鍵的索引,next key lock會降級成行鎖,而不會鎖住一個區間。因此,如果上面的事務1的update使用的是主鍵,事務2也使用主鍵進行插入,那么實際上事務2根本不會被阻塞,可以立即插入並返回。而對於非唯一索引,next key lock則不會降級。
9、 結論:
-
MySQL InnoDB的可重復讀並不保證避免幻讀,需要應用使用加鎖讀來保證。而這個加鎖讀使用到的機制就是next-key locks。
-
Read Committed隔離級別:每次select都生成一個快照讀。
-
Read Repeatable隔離級別:開啟事務后第一個select語句才是快照讀的地方,而不是一開啟事務就快照讀。
-
在RR級別下,快照讀是通過MVVC(多版本控制)和undo log來實現的,當前讀是通過加record lock(記錄鎖)和gap lock(間隙鎖)來實現的。
-
在mysql中,提供了兩種事務隔離技術,第一個是mvcc,第二個是next-key技術。這個在使用不同的語句的時候可以動態選擇。不加lock inshare mode之類的快照讀就使用mvcc。否則 當前讀使用next-key。mvcc的優勢是不加鎖,並發性高。缺點是不是實時數據。next-key的優勢是獲取實時數據,但是需要加鎖。
-
在rr級別下,mvcc完全解決了重復讀,但並不能真正的完全避免幻讀,只是在部分場景下利用歷史數據規避了幻讀
-
要完全避免幻讀,需要手動加鎖將快照讀調整為當前讀(mysql不會自動加鎖),然后mysql使用next-key完全避免了幻讀,比如rr下,鎖1(0,2,3,4),另一個線程的insert 3即被阻塞,在rc下,另一個線程仍然可以大搖大擺的插入,如本線程再次查詢比如count,則會不一致。