序
最近在學習MySQL中的MVCC,看了網上的各種版本,什么創建版本號、刪除版本號,一開始看的時候,好像很對的樣子,但實際上很多都是錯誤的。經過好幾天的查閱對比,在幾篇博客的幫助下,才算是覺得正確理解了MySQL中的MVCC。
本文是對MVCC的一些總結,並找到相關源碼佐證(talk is cheap,show me the code!網上錯誤的解釋實在是太多了)。如果你剛接觸MVCC,或者是被網上的各種解釋弄得快要暈了,請堅持看到最后,一定會對你有收獲。
目錄
1、MVCC概念
1.1、隱藏字段
1.2、Read View 結構(重點)
1.3、Undo log
2、記錄行修改的具體流程
3、可見性比較算法
4、當前讀和快照讀
5、例子(幫助理解)
1、MVCC概念
多版本控制(Multiversion Concurrency Control): 指的是一種提高並發的技術。最早的數據庫系統,只有讀讀之間可以並發,讀寫,寫讀,寫寫都要阻塞。引入多版本之后,只有寫寫之間相互阻塞,其他三種操作都可以並行,這樣大幅度提高了InnoDB的並發度。在內部實現中,InnoDB通過undo log保存每條數據的多個版本,並且能夠找回數據歷史版本提供給用戶讀,每個事務讀到的數據版本可能是不一樣的。在同一個事務中,用戶只能看到該事務創建快照之前已經提交的修改和該事務本身做的修改。
MVCC只在 Read Committed 和 Repeatable Read兩個隔離級別下工作。其他兩個隔離級別和MVCC不兼容,Read Uncommitted總是讀取最新的記錄行,而不是符合當前事務版本的記錄行;Serializable 則會對所有讀取的記錄行都加鎖。
MySQL的InnoDB存儲引擎默認事務隔離級別是RR(可重復讀),是通過 "行級鎖+MVCC"一起實現的,正常讀的時候不加鎖,寫的時候加鎖。而 MCVV 的實現依賴:隱藏字段、Read View、Undo log。
1.1、隱藏字段
InnoDB存儲引擎在每行數據的后面添加了三個隱藏字段:
1. DB_TRX_ID(6字節):表示最近一次對本記錄行作修改(insert | update)的事務ID。至於delete操作,InnoDB認為是一個update操作,不過會更新一個另外的刪除位,將行表示為deleted。並非真正刪除。
2. DB_ROLL_PTR(7字節):回滾指針,指向當前記錄行的undo log信息
3. DB_ROW_ID(6字節):隨着新行插入而單調遞增的行ID。理解:當表沒有主鍵或唯一非空索引時,innodb就會使用這個行ID自動產生聚簇索引。如果表有主鍵或唯一非空索引,聚簇索引就不會包含這個行ID了。這個DB_ROW_ID跟MVCC關系不大。
隱藏字段並不是什么創建版本、刪除版本。官方文檔:14.3 InnoDB Multi-Versioning
.
1.2、Read View 結構(重點)
其實Read View(讀視圖),跟快照、snapshot是一個概念。
Read View主要是用來做可見性判斷的, 里面保存了“對本事務不可見的其他活躍事務”。
Read View 結構源碼,其中包括幾個變量,在網上這些變量的解釋各種各樣,下面我結合源碼給出它們正確的解釋。
① low_limit_id:目前出現過的最大的事務ID+1,即下一個將被分配的事務ID。源碼 350行:
max_trx_id的定義如下,源碼 628行,翻譯過來就是“還未分配的最小事務ID”,也就是下一個將被分配的事務ID。(low_limit_id 並不是活躍事務列表中最大的事務ID)
② up_limit_id:活躍事務列表trx_ids中最小的事務ID,如果trx_ids為空,則up_limit_id 為 low_limit_id。源碼 358行:
因為trx_ids中的活躍事務號是逆序的,所以最后一個為最小活躍事務ID。(up_limit_id 並不是已提交的最大事務ID+1,后面的 例子2 會證明這是錯誤的)
③ trx_ids:Read View創建時其他未提交的活躍事務ID列表。意思就是創建Read View時,將當前未提交事務ID記錄下來,后續即使它們修改了記錄行的值,對於當前事務也是不可見的。
注意:Read View中trx_ids的活躍事務,不包括當前事務自己和已提交的事務(正在內存中),源碼 295行:
④ creator_trx_id:當前創建事務的ID,是一個遞增的編號,源碼 345行 。(這個編號並不是DB_ROW_ID)
1.3、Undo log
Undo log中存儲的是老版本數據,當一個事務需要讀取記錄行時,如果當前記錄行不可見,可以順着undo log鏈找到滿足其可見性條件的記錄行版本。
大多數對數據的變更操作包括 insert/update/delete,在InnoDB里,undo log分為如下兩類:
①insert undo log : 事務對insert新記錄時產生的undo log, 只在事務回滾時需要, 並且在事務提交后就可以立即丟棄。
②update undo log : 事務對記錄進行delete和update操作時產生的undo log,不僅在事務回滾時需要,快照讀也需要,只有當數據庫所使用的快照中不涉及該日志記錄,對應的回滾日志才會被purge線程刪除。
Purge線程:為了實現InnoDB的MVCC機制,更新或者刪除操作都只是設置一下舊記錄的deleted_bit,並不真正將舊記錄刪除。
為了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit為true的記錄。purge線程自己也維護了一個read view,如果某個記錄的deleted_bit為true,並且DB_TRX_ID相對於purge線程的read view可見,那么這條記錄一定是可以被安全清除的。
2、記錄行修改的具體流程
假設有一條記錄行如下,字段有Name和Honor,值分別為"Curry"和"MVP",行ID是1,最新修改這條記錄的事務ID為1。
(1)現在事務A(事務ID為2)對該記錄的Honor做出了修改,將Honor改為"FMVP":
①事務A先對該行加排它鎖
②然后把該行數據拷貝到undo log中,作為舊版本
③拷貝完畢后,修改該行的Honor為"FMVP",並且修改DB_TRX_ID為2(事務A的ID), 回滾指針指向拷貝到undo log的舊版本。(然后還會將修改后的最新數據寫入redo log)
④事務提交,釋放排他鎖
(2) 接着事務B(事務ID為3)修改同一個記錄行,將Name修改為"Iguodala":
①事務B先對該行加排它鎖
②然后把該行數據拷貝到undo log中,作為舊版本
③拷貝完畢后,修改該行Name為"Iguodala",並且修改DB_TRX_ID為3(事務B的ID), 回滾指針指向拷貝到undo log最新的舊版本。
④事務提交,釋放排他鎖
從上面可以看出,不同事務或者相同事務的對同一記錄行的修改,會使該記錄行的undo log成為一條鏈表,undo log的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄。
3、可見性比較算法
在innodb中,創建一個新事務后,執行第一個select語句的時候,innodb會創建一個快照(read view),快照中會保存系統當前不應該被本事務看到的其他活躍事務id列表(即trx_ids)。當用戶在這個事務中要讀取某個記錄行的時候,innodb會將該記錄行的DB_TRX_ID與該Read View中的一些變量進行比較,判斷是否滿足可見性條件。
假設當前事務要讀取某一個記錄行,該記錄行的DB_TRX_ID(即最新修改該行的事務ID)為trx_id,Read View的活躍事務列表trx_ids中最早的事務ID為up_limit_id,將在生成這個Read Vew時系統出現過的最大的事務ID+1記為low_limit_id(即還未分配的事務ID)。
具體的比較算法如下:
1. 如果 trx_id < up_limit_id, 那么表明“最新修改該行的事務”在“當前事務”創建快照之前就提交了,所以該記錄行的值對當前事務是可見的。跳到步驟5。
2. 如果 trx_id >= low_limit_id, 那么表明“最新修改該行的事務”在“當前事務”創建快照之后才修改該行,所以該記錄行的值對當前事務不可見。跳到步驟4。
3. 如果 up_limit_id <= trx_id < low_limit_id, 表明“最新修改該行的事務”在“當前事務”創建快照的時候可能處於“活動狀態”或者“已提交狀態”;所以就要對活躍事務列表trx_ids進行查找(源碼中是用的二分查找,因為是有序的):
(1) 如果在活躍事務列表trx_ids中能找到 id 為 trx_id 的事務,表明在“當前事務”創建快照前,“該記錄行的值”被“id為trx_id的事務”修改了,但沒有提交;或者在“當前事務”創建快照后,“該記錄行的值”被“id為trx_id的事務”修改了(不管有無提交);這些情況下,這個記錄行的值對當前事務都是不可見的,跳到步驟4;
(2)在活躍事務列表中找不到,則表明“id為trx_id的事務”在修改“該記錄行的值”后,在“當前事務”創建快照前就已經提交了,所以記錄行對當前事務可見,跳到步驟5。
4. 在該記錄行的 DB_ROLL_PTR 指針所指向的undo log回滾段中,取出最新的的舊事務號DB_TRX_ID, 將它賦給trx_id,然后跳到步驟1重新開始判斷。
5. 將該可見行的值返回。
(可以照着后面的 例子 ,看這段)
比較算法源碼 84行,也可看下圖,有注釋,圖代碼來自 link:
4、當前讀和快照讀
快照讀(snapshot read):普通的 select 語句(不包括 select ... lock in share mode, select ... for update)
當前讀(current read) :select ... lock in share mode,select ... for update,insert,update,delete 語句(這些語句獲取的是數據庫中的最新數據,官方文檔:14.7.2.4 Locking Reads )
只靠 MVCC 實現RR隔離級別,可以保證可重復讀,還能防止部分幻讀,但並不是完全防止。
比如事務A開始后,執行普通select語句,創建了快照;之后事務B執行insert語句;然后事務A再執行普通select語句,得到的還是之前B沒有insert過的數據,因為這時候A讀的數據是符合快照可見性條件的數據。這就防止了部分幻讀,此時事務A是快照讀。
但是,如果事務A執行的不是普通select語句,而是select ... for update等語句,這時候,事務A是當前讀,每次語句執行的時候都是獲取的最新數據。也就是說,在只有MVCC時,A先執行 select ... where nid between 1 and 10 … for update;然后事務B再執行 insert … nid = 5 …;然后 A 再執行 select ... where nid between 1 and 10 … for update,就會發現,多了一條B insert進去的記錄。這就產生幻讀了,所以單獨靠MVCC並不能完全防止幻讀。
因此,InnoDB在實現RR隔離級別時,不僅使用了MVCC,還會對“當前讀語句”讀取的記錄行加記錄鎖(record lock)和間隙鎖(gap lock),禁止其他事務在間隙間插入記錄行,來防止幻讀。也就是前文說的"行級鎖+MVCC"。
如果你對這些鎖不是很熟悉,這是一篇將MySQL 中鎖機制講的很詳細的博客 。
RR和RC的Read View產生區別:
①在innodb中的Repeatable Read級別, 只有事務在begin之后,執行第一條select(讀操作)時, 才會創建一個快照(read view),將當前系統中活躍的其他事務記錄起來;並且事務之后都是使用的這個快照,不會重新創建,直到事務結束。
②在innodb中的Read Committed級別, 事務在begin之后,執行每條select(讀操作)語句時,快照會被重置,即會重新創建一個快照(read view)。
官方文檔:consistent read,里面所說的consistent read 一致性讀,我的理解就是 快照讀,也就是普通select語句,它們不會對訪問的數據加鎖。 只有普通select語句才會創建快照,select ... lock in share mode,select ... for update不會,update、delete、insert語句也不會,因為它們都是 當前讀,會對訪問的數據加鎖。
5、例子(幫助理解)
假設原始數據行:
Field DB_ROW_ID DB_TRX_ID DB_ROLL_PTR
0 10 10000 0x13525342
例子1
例子2
(證明“up_limit_id為已提交的最大事務ID + 1”是錯誤的)
例子3
(跟例子2一樣的情況,不過up_limit_id變為trx_ids中最小的事務ID):
參考:
MySQL-InnoDB-MVCC多版本並發控制
MySQL數據庫事務各隔離級別加鎖情況--read committed && MVCC
InnoDB存儲引擎MVCC的工作原理
【MySQL筆記】正確的理解MySQL的MVCC及實現原理
Mysql Innodb中undo-log和MVCC多版本一致性讀 的實現
InnoDB事務分析-MVCC
————————————————
版權聲明:本文為CSDN博主「Waves___」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/Waves___/article/details/105295060