图文结合带你搞懂InnoDB MVCC
- GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯系小編并注明來源。
- GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。
-
- 前情提要
- 當前讀
- 快照讀
- 什么是MVCC
- 三個隱藏字段
- Undo Log回滾日志
- MVCC版本鏈
- ReadView讀視圖
- 不同隔離級別下MVCC分析
- READ-COMMITTED隔離級別
- REPEATABLE-READ隔離級別
前情提要
事務有四大特性ACID分別是:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)
其中隔離性是通過數據庫的鎖加上MVCC(多版本并發控制)來保證的。
在介紹MVCC之前先來了解一下當前讀和快照讀。
當前讀
當前讀讀取的是記錄的最新版本。同時在讀取的時候還要保證其他的并發事務不能更改當前記錄,那么當前讀會對它要讀取的記錄進行加鎖。不同的操作會加上不同類型的鎖,如:SELECT ... LOCK IN SHARE MODE(共享鎖),SELECT ... FOR UPDATE、UPDATE、INSERT、 DELETE(排他鎖)。
快照讀
簡單的不加鎖的SELECT就是快照讀,快照讀讀取的是快照生成時的數據,不一定是最新的數據,它是不加鎖的非阻塞讀。而不同隔離級別下,創建快照的時機也不同:
- READ-COMMITTED(讀已提交):事務每次SELECT時創建ReadView
- REPEATABLE-READ(可重復讀):事務第一次SELECT時創建ReadView,后續一直使用
在MySQL默認隔離級別(REPEATABLE-READ)下,快照讀保證了數據的可重復讀。
什么是MVCC
MVCC全稱Multi-Version Concurrency Control,即多版本并發控制。它是一種并發控制的方法,它可以維護一個數據的多個版本,用更好的方式去處理讀寫沖突,做到即使有讀寫沖突也能不加鎖。MySQL中MVCC的具體實現,還需要依賴于表中的三個隱藏字段、Undo Log日志以及ReadView。
三個隱藏字段
mysql> SHOW CREATE TABLE stu \G; *************************** 1. row ***************************Table: stu Create Table: CREATE TABLE `stu` (`id` int NOT NULL,`name` varchar(10) NOT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec)mysql> SELECT * FROM stu; +----+--------+ | id | name | +----+--------+ | 1 | m | | 2 | f | +----+--------+當創建了上述這張表后,我們在查看表結構時只能看到id、name字段,實際上除了這兩個字段外,InnoDB引擎還自動為我們添加了三個隱藏字段,見下表:
| DB_TRX_ID | 最近修改事務ID,記錄插入這條記錄或最后一次修改該記錄的事務ID。 |
| DB_ROLL_PTR | 回滾指針,指向這條記錄的上一個版本,用于配合Undo Log,指向上一個版本。 |
| DB_ROW_ID | 隱藏主鍵,如果表結構沒有指定主鍵,將會生成該隱藏字段。 |
我們可以使用ibd2sdi工具來從表空間文件中提取序列化的字典信息(SDI),來驗證一下這三個隱藏字段是否存在。
["ibd2sdi" , {"type": 1,"id": 402,"object":{"mysqld_version_id": 80025,"dd_version": 80023,"sdi_version": 80019,"dd_object_type": "Table","dd_object": {"name": "stu","mysql_version_id": 80025,"created": 20220919023413,"last_altered": 20220919023413,"hidden": 1,"options": "avg_row_length=0;encrypt_type=N;explicit_encryption=0;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;","columns": [{"name": "id","type": 4,"is_nullable": false,"is_zerofill": false,"is_unsigned": false,"is_auto_increment": false,"is_virtual": false,"hidden": 1, ···省略},{"name": "name","type": 16,"is_nullable": false,"is_zerofill": false,"is_unsigned": false,"is_auto_increment": false,"is_virtual": false,"hidden": 1, ···省略},{"name": "DB_TRX_ID", #最近修改事務ID"type": 10,"is_nullable": false,"is_zerofill": false,"is_unsigned": false,"is_auto_increment": false,"is_virtual": false,"hidden": 2, ···省略},{"name": "DB_ROLL_PTR", #回滾指針"type": 9,"is_nullable": false,"is_zerofill": false,"is_unsigned": false,"is_auto_increment": false,"is_virtual": false,"hidden": 2, ···省略}],注意:因為這張表里已經指定了主鍵為id列,所以不會生成隱藏主鍵DB_ROW_ID列。
Undo Log回滾日志
回滾日志,在增、改、刪操作的時候產生的便于數據回滾的日志。當INSERT操作的時候,產生的回滾日志在事務提交后可被立即刪除。而UPDATE和DELETE操作的時候,產生的Undo Log日志不僅在進行數據回滾時需要,在進行快照讀時也需要,所以不會立即被刪除。
Undo Log詳情可見文章:待浩源Undo Log文章發表后添加
MVCC版本鏈
當有多個并發事務操作一行數據時,對這行數據的修改會產生多個版本,多個版本通過上述的一個隱藏字段DB_ROLL_PTR回滾指針指向Undo Log數據地址形成一個鏈表,即MVCC版本鏈。
ReadView讀視圖
ReadView讀視圖是快照讀SQL執行時MVCC提取數據的依據,記錄并維護系統當前活躍的事務(未提交的)id。
上面講過Undo Log和MVCC版本鏈,一條數據經過多次修改會產生多個版本,而快照讀是根據不同時機創建的快照獲取數據的,那么快照讀SQL在執行時該讀取那個版本的數據就是靠ReadViw讀視圖來決定的。
ReadView讀視圖中包含了四個核心字段,也是讀取數據的判斷依據: | 字段 | 含義 | | -------------- | ---------------------------------------------------- | | m_ids | 當前活躍的事務ID集合 | | min_trx_id | 最小活躍事務ID | | max_trx_id | 預分配事務ID,當前最大事務ID+1(因為事務ID是自增的) | | creator_trx_id | ReadView創建者的事務ID |
ReadView一共有四種匹配規則: | 條件 | 能否訪問 | 說明 | | ---------------------------------- | ----------------------------------------- | -------------------------------------------- | | trx_id == creatro_trx_id | 可以訪問該版本 | 成立,說明數據是當前這個事務更改的。 | | trx_id < min_trx_id | 可以訪問該版本 | 成立,說明數據已經提交了。 | | trx_id > max_trx_id | 不可以訪問該版本 | 成立,說明該事務是在ReadView生成后才開啟的。 | | min_trx_id <= trx_id <= max_trx_id | 如果trx_id不在m_ids中,那么可以訪問該版本 | 成立,說明數據已經提交。 |
不同隔離級別下MVCC分析
READ-COMMITTED隔離級別
前面有提到過在READ-COMMITTED隔離級別下事務在每次快照讀SQL執行時創建ReadView,每次創建的ReadView的四個字段對應的值也是不同的,所以在READ-COMMITTED隔離級別下每次快照讀SQL獲取的數據可能也是不同的。
下面通過一個READ-COMMITTED隔離級別下并發事務的案例來詳細看看:
現有四個并發事務同時訪問一條數據:
在上述并發事務中,事務5查詢了兩次id為1的數據,因為當前的隔離級別設置為了READ-COMMITTED,事務在每次快照讀SQL執行時創建一個ReadView,每次生成的ReadView中的四個字段值都不同。那么三次快照讀都會根據生成的ReadView中的字段進行規則匹配,從而決定返回的數據。接下來看看流程:
事務5第一次快照讀解讀
事務5第一次進行查詢時生成的ReadView以及原數據如下圖:
在匹配版本數據前,先與表中數據進行匹配:
該數據對應的DB_TRX_ID為3,此時MVCC就會通過ReadView帶著這條數據去進行規則匹配:
首先是第一條規則db_trx_id == creator_trx_id,db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id < min_trx_id,db_trx_id(3)不小于min_trx_id(3)故不成立;
第三條規則db_trx_id > max_trx_id,db_trx_id(3)小于max_trx_id(6)故不成立;
第四條規則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(3)在min_trx(3)與max_trx_id(6)之間,但是同時處于m_ids(3,4,5)集合之中故也不成立。
經過這次匹配,表中最新的數據無法匹配,故要與MVCC版本鏈中最上面的數據進行規則匹配
與MVCC版本鏈中最上方的版本進行匹配:
第一條規則db_trx_id(2)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id(2)小于min_trx_id(3),該版本的數據滿足匹配規則中的第二條,說明數據已經提交,此時匹配將終止并返回這個版本對應的數據。
事務5第二次快照讀
因為當前事務的隔離級別為READ-COMMITTED(讀已提交),所以在每次快照讀的時候都會創建一個ReadView,所以事務5第二次進行查詢時生成的ReadView以及原數據如下圖:
在匹配版本數據前,先與表中數據進行匹配:
該數據對應的DB_TRX_ID為4,此時MVCC就會通過ReadView帶著這條數據去進行規則匹配:
首先是第一條規則db_trx_id == creator_trx_id,db_trx_id(4)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id < min_trx_id,db_trx_id(4)不小于min_trx_id(4)故不成立;
第三條規則db_trx_id > max_trx_id,db_trx_id(4)小于max_trx_id(6)故不成立;
第四條規則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(4)在min_trx(4)與max_trx_id(6)之間,但是同時處于m_ids(4,5)集合之中故也不成立。
經過這次匹配,表中最新的數據無法匹配,故要與MVCC版本鏈中最上面的數據進行規則匹配
與MVCC版本鏈中最上方的版本進行匹配:
第一條規則db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id(3)小于min_trx_id(4),該版本的數據滿足匹配規則中的第二條,說明數據已經提交,此時匹配將終止并返回這個版本對應的數據。
REPEATABLE-READ級別
現在來看看REPEATABLE-READ可重復讀隔離級別有什么不同的地方。同樣,有四個并發事務同時訪問一條數據:
在上述并發事務中,事務5查詢了兩次id為1的數據,因為當前的隔離級別設置為了REPEATABLE-READ,事務在第一次快照讀SQL執行時創建ReadView,后續該事務所有的快照讀都復用該ReadView。接下來看看流程:
事務5第一次快照讀解讀
事務5第一次進行查詢時生成的ReadView以及原數據如下圖:
在匹配版本數據前,先與表中數據進行匹配:
該數據對應的DB_TRX_ID為3,此時MVCC就會通過ReadView帶著這條數據去進行規則匹配:
首先是第一條規則db_trx_id == creator_trx_id,db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id < min_trx_id,db_trx_id(3)不小于min_trx_id(3)故不成立;
第三條規則db_trx_id > max_trx_id,db_trx_id(3)小于max_trx_id(6)故不成立;
第四條規則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(3)在min_trx(3)與max_trx_id(6)之間,但是同時處于m_ids(3,4,5)集合之中故也不成立。
經過這次匹配,表中最新的數據無法匹配,故要與MVCC版本鏈中最上面的數據進行規則匹配
與MVCC版本鏈中最上方的版本進行匹配:
第一條規則db_trx_id(2)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id(2)小于min_trx_id(3),該版本的數據滿足匹配規則中的第二條,說明數據已經提交,此時匹配將終止并返回這個版本對應的數據。
事務5第二次快照讀解讀
因為當前事務的隔離級別為REPEATABLE-READ(可重復讀),所以第二次快照讀也會沿用第一次快照讀時創建的ReadView,如下:
在匹配版本數據前,先與表中數據進行匹配:
該數據對應的DB_TRX_ID為4,此時MVCC就會通過ReadView帶著這條數據去進行規則匹配:
首先是第一條規則db_trx_id == creator_trx_id,db_trx_id(4)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id < min_trx_id,db_trx_id(4)不小于min_trx_id(3)故不成立;
第三條規則db_trx_id > max_trx_id,db_trx_id(4)小于max_trx_id(6)故不成立;
第四條規則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(4)在min_trx(4)與max_trx_id(6)之間,但是同時處于m_ids(4,5)集合之中故也不成立。
經過這次匹配,表中最新的數據無法匹配,故要與MVCC版本鏈中最上面的數據進行規則匹配
與MVCC版本鏈中最上方的版本進行匹配:
第一條規則db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id(3)不小于min_trx_id(4)故不成立;
第三條規則db_trx_id小于max_trx_id(6)故不成立;
第四條規則db_trx_id(3)在min_trx(3)與max_trx_id(6)之間,但是同時處于m_ids(3,4,5)集合之中故也不成立。
經過第二次匹配,MVCC版本鏈中最上層的數據版本也無法匹配,故要與第二條版本進行匹配
與MVCC版本鏈中第二條版本進行匹配:
第一條規則db_trx_id(2)不等于creator_trx_id(5)故不成立;
第二條規則db_trx_id(2)小于min_trx_id(3),該版本的數據滿足匹配規則中的第二條,說明數據已經提交,此時匹配將終止并返回這個版本對應的數據。
Enjoy GreatSQL :)
關于 GreatSQL
GreatSQL是由萬里數據庫維護的MySQL分支,專注于提升MGR可靠性及性能,支持InnoDB并行查詢特性,是適用于金融級應用的MySQL分支版本。
相關鏈接: GreatSQL社區 Gitee GitHub Bilibili
GreatSQL社區:
歡迎來GreatSQL社區發帖提問 https://greatsql.cn/
技術交流群:
微信:掃碼添加GreatSQL社區助手微信好友,發送驗證信息加群。
總結
以上是生活随笔為你收集整理的图文结合带你搞懂InnoDB MVCC的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构代码
- 下一篇: C++ | PaddleOCR GPU版