一、什么是MVCC?
MVCC,全稱Multi-Version Concurrency Control,即多版本並發控制。MVCC是一種並發控制的方法,一般在數據庫管理系統中,實現對數據庫的並發訪問多版本控制: 指的是一種提高並發的技術。最早的數據庫系統,只有讀讀之間可以並發,讀寫,寫讀,寫寫都要阻塞。引入多版本之后,只有寫寫之間相互阻塞,其他三種操作都可以並行,這樣大幅度提高了InnoDB的並發度。在內部實現中,與Postgres在數據行上實現多版本不同,InnoDB是在undo log中實現的,通過undo log可以找回數據的歷史版本。找回的數據歷史版本可以提供給用戶讀(按照隔離級別的定義,有些讀請求只能看到比較老的數據版本),也可以在回滾的時候覆蓋數據頁上的數據。在InnoDB內部中,會記錄一個全局的活躍讀寫事務數組,其主要用來判斷事務的可見性。
MVCC用於實現提交讀和可重復讀這兩種隔離級別。而未提交讀隔離級別總是讀取最新的數據行,無需使用 MVCC。可串行化隔離級別需要對所有讀取的行都加鎖,單純使用 MVCC 無法實現。
基本思想:MVCC 利用了多版本的思想,寫操作更新最新的版本快照,而讀操作去讀舊版本快照,沒有互斥關系,這一點和 CopyOnWrite 類似。
二、什么是當前讀和快照讀?
當前讀:就是它讀取的是記錄的最新版本,讀取時還要保證其他並發事務不能修改當前記錄,會對讀取的記錄進行加鎖
快照讀:可能讀到的並不一定是數據的最新版本,而有可能是之前的歷史版本
三、MVCC的實現原理
MVCC的目的就是多版本並發控制,在數據庫中的實現,就是為了解決讀寫沖突,它的實現原理主要是依賴記錄中的 3個隱式字段,undo log ,Read View 來實現的
1、3個隱式字段
每行記錄除了我們自定義的字段外,還有數據庫隱式定義的字段:
- DB_ROW_ID用於標識記錄的唯一ID,在InnoDB表中對應主鍵列或一個隱藏的主鍵列(隱含的自增ID);
- DB_TRX_ID表示修改該記錄的事務ID;
- DB_ROLL_PTR回滾指針,指向該記錄的undo log。也就是指向這條記錄的上一個版本。
2、undo log
undo log是一個用於存放事務執行前數據的備份的日志,在事務回滾時可以使用該日志來恢復到事務執行前的狀態。
undo log主要分為兩種:
- insert undo log
代表事務在insert新記錄時產生的undo log, 只在事務回滾時需要,並且在事務提交后可以被立即丟棄- update undo log
事務在進行update或delete時產生的undo log; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快照讀或事務回滾不涉及該日志時,對應的日志才會被purge線程統一清除
不同事務或者相同事務的對同一記錄的修改,會導致該記錄的undo log成為一條記錄版本線性表,既鏈表,undo log的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄
3、Read View
Read View用於記錄每個事務開始時間的數據結構,以便 MySQL 引擎在查詢時能夠根據事務啟動時間戳和各個數據行對應的版本鏈信息來確定該事務能夠看到哪些數據。
通俗的說,當一個事務開始執行時,MySQL引擎會記錄下該事務開始的時間戳。之后,在這個事務執行的過程中,如果需要讀取某個數據行,MySQL引擎會根據這個時間戳和該數據行對應的版本鏈信息,找到最近的、在該事務啟動時間之前已經提交的數據版本。這樣,就可以保證在該事務的執行過程中,讀取的數據是和該事務啟動時一致的。
需要注意的是,每個事務都會有自己的Read View,它們是相互獨立的。因此,不同的事務對同一行數據的讀取結果可能是不同的,這取決於它們啟動的時間。
總之,Read View提供了一個機制,用於確定在某個事務啟動時間之前已經提交的數據版本,以便實現可重復讀和串行化隔離級別下的並發讀取。
拓展
Read View就是事務進行快照讀操作的時候生產的讀視圖,在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個trx_id, 這個trx_id是遞增的,所以最新的事務,trx_id值越大)
所以我們知道 Read View主要是用來做可見性判斷的, 即當我們某個事務執行快照讀的時候,對該記錄創建一個Read View讀視圖,把它比作條件用來判斷當前事務能夠看到哪個版本的數據,既可能是當前最新的數據,也有可能是該行記錄的undo log里面的某個版本的數據。
Read View 是一個結構體,包含多個字段。其中比較重要的字段有以下4個:
- trx_ids
InnoDB 為每個事務構造了一個數組trx_ids,用來保存這個事務啟動瞬間,當前正在“活躍”的所有事務 ID。(“活躍”指的就是,啟動了但還沒提交。)- low_limit_id
trx_ids數組中的最小事務ID。- up_limit_id
ReadView生成時刻系統尚未分配的下一個事務ID的值,也就是目前已出現過的事務ID的最大值+1- creator_trx_id
創建這個 ReadView 的事務ID。
數組里面事務 ID 的最小值記為低水位,當前系統里面已經創建過的事務 ID 的最大值加 1 記為高水位。這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。
低水位和高水位將事務分成了三段:
已提交事務 未提交事務(所有活躍事務的數組 trx_ids) 未開始事務 Read View 通過判斷這四個字段的方式來確定哪些數據對於當前事務來說是可見的,哪些是不可見的。判斷過程如下:
1) 如果被訪問版本的trx_id,與readview中的creator_trx_id值相同,表明當前事務在訪問自己修改過的記錄,該版本可以被當前事務訪問
2)如果落在綠色部分,表示這個版本是已提交的事務生成的,這個數據是可見的;
3)如果落在紅色部分,表示這個版本是由將來啟動的事務生成的,是不可見的;
4)如果落在黃色部分(low_limit_id <= trx_id <= up_limit_id),那就包括兩種情況
a. 若 trx_id 在數組trx_ids中,表示這個版本是由還沒提交的事務生成的,不可見;
b. 若 trx_id 不在數組中trx_ids,表示這個版本是已經提交了的事務生成的,可見。
【總之就是修改當前行的事務提交了,數據才能被其他事務查看】
四、RC,RR級別下的InnoDB快照讀有什么不同?
正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同
- 在RR【可重復讀】級別下的某個事務的對某條記錄的第一次快照讀會創建一個Read View, 將當前系統活躍的其他事務記錄起來,此后在調用快照讀的時候,還是使用的是同一個Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那么之后的快照讀使用的都是同一個Read View,所以對之后的修改不可見;【即事務在RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動事務的快照,這些事務的修改對於當前事務都是不可見的。而早於Read View創建的事務所做的修改均是可見】
- 事務在RC【讀已提交】級別下,每次快照讀都會新生成一個Read View, 這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因
總之在RC隔離級別下,是每個快照讀都會生成並獲取最新的Read View;而在RR隔離級別下,則是同一個事務中的第一個快照讀才會創建Read View, 之后的快照讀獲取的都是同一個Read View。
五、臨鍵鎖(Next-Key Locks)
在了解臨鍵鎖前,先熟悉一下記錄鎖和間隙鎖:
1、記錄鎖(Record Locks)
記錄鎖其實很好理解,對表中的記錄加鎖,叫做記錄鎖,簡稱行鎖。比如:
SELECT * FROM `test` WHERE `id` = 1 FOR UPDATE;
它會在 id=1 的記錄上加上記錄鎖,以阻止其他事務插入,更新,刪除 id=1 這一行。
需要注意的是:
- id 列必須為唯一索引列或主鍵列,否則上述語句加的鎖就會變成臨鍵鎖(有關臨鍵鎖下面會講)。
- 同時查詢語句必須為精准匹配(=),不能為 >、<、like等,否則也會退化成臨鍵鎖。
其他實現:在通過 主鍵索引 與 唯一索引 對數據行進行 UPDATE 操作時,也會對該行數據加行鎖:
-- id 列為主鍵列或唯一索引列
UPDATE SET age = 50 WHERE id = 1;
鎖定的是一個記錄上的索引,而不是記錄本身。如果要鎖的列沒有索引,則進行全表記錄加鎖。
2、間隙鎖(Gap Locks)
間隙鎖 是 Innodb 在 可重復讀隔離級別下為了解決幻讀問題時引入的鎖機制。間隙鎖是innodb中行鎖的一種。
注意:使用間隙鎖鎖住的是一個區間,而不僅僅是這個區間中的每一條數據。也就是鎖定索引之間的間隙,但是不包含索引本身
舉例來說,假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101,下面的SQL:
SELECT * FROM emp WHERE empid > 100 FOR UPDATE
當我們用條件檢索數據,並請求共享或排他鎖時,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大於101(即使這些記錄並不存在)的“間隙”加鎖。
這個時候如果你插入empid等於102的數據的,如果那邊事物還沒有提交,那你就會處於等待狀態,無法插入數據。
3、臨鍵鎖(Next-Key Locks)
Tips:
1) 共享鎖
共享鎖(Shared Lock)也叫讀鎖,是用於讀取數據的鎖定方式。當一個事務獲取了一行數據的共享鎖后,其他事務還可以繼續獲取該行的共享鎖,但是不能獲取獨占鎖(排它鎖),也即是其他事務不能修改該行的數據。
在數據庫中,多個事務可以同時獲取同一行的共享鎖,實現多個事務同時讀取同一行數據的操作,所以共享鎖也稱為讀共享鎖。
2) 行鎖
行鎖(Row Lock)也叫寫鎖或獨占鎖(Exclusive Lock),是用於修改數據的鎖定方式。當一個事務獲取了一行數據的行鎖后,其他事務無法獲取該行的任何鎖,也無法修改該行的數據。
在數據庫中,一般只允許一個事務獲取同一行的行鎖,確保操作的數據一致性,所以行鎖也稱為寫鎖或獨占鎖。
3) 區別
共享鎖和行鎖的區別在於它們的使用方式和作用對象。共享鎖用於讀取數據,多個事務可以同時獲取同一行的共享鎖,實現並發讀取同一數據的操作;行鎖用於修改數據,只允許一個事務獲取同一行的行鎖,確保數據的一致性。
Next-Key Locks 是 MySQL 的 InnoDB 存儲引擎的一種鎖實現。它是行鎖和間隙鎖的組合,不僅鎖定一個記錄上的索引,也鎖定索引之間的間隙。它鎖定一個前開后閉區間,例如一個索引包含以下值:10, 11, 13, 20,那么就需要鎖定以下區間:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
MVCC 不能解決幻影讀問題,Next-Key Locks 就是為了解決這個問題而存在的。在可重復讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Locks 可以解決幻讀問題。總之,Next-Key Locks是MySQL中實現MVCC的重要鎖機制,在可重復讀隔離級別下,能夠保證讀取數據時只能看到自己啟動時間之前已經提交的數據,並能夠避免幻讀的發生。
4、總結
這里對 記錄鎖(行鎖)、間隙鎖、臨鍵鎖 做一個總結
- InnoDB 中的行鎖的實現依賴於索引,一旦某個加鎖操作沒有使用到索引,那么該鎖就會退化為表鎖。
- 記錄鎖存在於包括主鍵索引在內的唯一索引中,鎖定單條索引記錄。
- 間隙鎖存在於非唯一索引中,鎖定開區間范圍內的一段間隔,它是基於臨鍵鎖實現的。
- 臨鍵鎖存在於非唯一索引中,該類型的每條記錄的索引上都存在這種鎖,它是一種特殊的間隙鎖,鎖定一段左開右閉的索引區間。