图文结合带你搞懂InnoDB MVCC
- GreatSQL社區(qū)原創(chuàng)內(nèi)容未經(jīng)授權(quán)不得隨意使用,轉(zhuǎn)載請(qǐng)聯(lián)系小編并注明來(lái)源。
- GreatSQL是MySQL的國(guó)產(chǎn)分支版本,使用上與MySQL一致。
-
- 前情提要
- 當(dāng)前讀
- 快照讀
- 什么是MVCC
- 三個(gè)隱藏字段
- Undo Log回滾日志
- MVCC版本鏈
- ReadView讀視圖
- 不同隔離級(jí)別下MVCC分析
- READ-COMMITTED隔離級(jí)別
- REPEATABLE-READ隔離級(jí)別
前情提要
事務(wù)有四大特性ACID分別是:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)
其中隔離性是通過(guò)數(shù)據(jù)庫(kù)的鎖加上MVCC(多版本并發(fā)控制)來(lái)保證的。
在介紹MVCC之前先來(lái)了解一下當(dāng)前讀和快照讀。
當(dāng)前讀
當(dāng)前讀讀取的是記錄的最新版本。同時(shí)在讀取的時(shí)候還要保證其他的并發(fā)事務(wù)不能更改當(dāng)前記錄,那么當(dāng)前讀會(huì)對(duì)它要讀取的記錄進(jìn)行加鎖。不同的操作會(huì)加上不同類型的鎖,如:SELECT ... LOCK IN SHARE MODE(共享鎖),SELECT ... FOR UPDATE、UPDATE、INSERT、 DELETE(排他鎖)。
快照讀
簡(jiǎn)單的不加鎖的SELECT就是快照讀,快照讀讀取的是快照生成時(shí)的數(shù)據(jù),不一定是最新的數(shù)據(jù),它是不加鎖的非阻塞讀。而不同隔離級(jí)別下,創(chuàng)建快照的時(shí)機(jī)也不同:
- READ-COMMITTED(讀已提交):事務(wù)每次SELECT時(shí)創(chuàng)建ReadView
- REPEATABLE-READ(可重復(fù)讀):事務(wù)第一次SELECT時(shí)創(chuàng)建ReadView,后續(xù)一直使用
在MySQL默認(rèn)隔離級(jí)別(REPEATABLE-READ)下,快照讀保證了數(shù)據(jù)的可重復(fù)讀。
什么是MVCC
MVCC全稱Multi-Version Concurrency Control,即多版本并發(fā)控制。它是一種并發(fā)控制的方法,它可以維護(hù)一個(gè)數(shù)據(jù)的多個(gè)版本,用更好的方式去處理讀寫沖突,做到即使有讀寫沖突也能不加鎖。MySQL中MVCC的具體實(shí)現(xiàn),還需要依賴于表中的三個(gè)隱藏字段、Undo Log日志以及ReadView。
三個(gè)隱藏字段
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 | +----+--------+當(dāng)創(chuàng)建了上述這張表后,我們?cè)诓榭幢斫Y(jié)構(gòu)時(shí)只能看到id、name字段,實(shí)際上除了這兩個(gè)字段外,InnoDB引擎還自動(dòng)為我們添加了三個(gè)隱藏字段,見(jiàn)下表:
| DB_TRX_ID | 最近修改事務(wù)ID,記錄插入這條記錄或最后一次修改該記錄的事務(wù)ID。 |
| DB_ROLL_PTR | 回滾指針,指向這條記錄的上一個(gè)版本,用于配合Undo Log,指向上一個(gè)版本。 |
| DB_ROW_ID | 隱藏主鍵,如果表結(jié)構(gòu)沒(méi)有指定主鍵,將會(huì)生成該隱藏字段。 |
我們可以使用ibd2sdi工具來(lái)從表空間文件中提取序列化的字典信息(SDI),來(lái)驗(yàn)證一下這三個(gè)隱藏字段是否存在。
["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", #最近修改事務(wù)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, ···省略}],注意:因?yàn)檫@張表里已經(jīng)指定了主鍵為id列,所以不會(huì)生成隱藏主鍵DB_ROW_ID列。
Undo Log回滾日志
回滾日志,在增、改、刪操作的時(shí)候產(chǎn)生的便于數(shù)據(jù)回滾的日志。當(dāng)INSERT操作的時(shí)候,產(chǎn)生的回滾日志在事務(wù)提交后可被立即刪除。而UPDATE和DELETE操作的時(shí)候,產(chǎn)生的Undo Log日志不僅在進(jìn)行數(shù)據(jù)回滾時(shí)需要,在進(jìn)行快照讀時(shí)也需要,所以不會(huì)立即被刪除。
Undo Log詳情可見(jiàn)文章:待浩源Undo Log文章發(fā)表后添加
MVCC版本鏈
當(dāng)有多個(gè)并發(fā)事務(wù)操作一行數(shù)據(jù)時(shí),對(duì)這行數(shù)據(jù)的修改會(huì)產(chǎn)生多個(gè)版本,多個(gè)版本通過(guò)上述的一個(gè)隱藏字段DB_ROLL_PTR回滾指針指向Undo Log數(shù)據(jù)地址形成一個(gè)鏈表,即MVCC版本鏈。
ReadView讀視圖
ReadView讀視圖是快照讀SQL執(zhí)行時(shí)MVCC提取數(shù)據(jù)的依據(jù),記錄并維護(hù)系統(tǒng)當(dāng)前活躍的事務(wù)(未提交的)id。
上面講過(guò)Undo Log和MVCC版本鏈,一條數(shù)據(jù)經(jīng)過(guò)多次修改會(huì)產(chǎn)生多個(gè)版本,而快照讀是根據(jù)不同時(shí)機(jī)創(chuàng)建的快照獲取數(shù)據(jù)的,那么快照讀SQL在執(zhí)行時(shí)該讀取那個(gè)版本的數(shù)據(jù)就是靠ReadViw讀視圖來(lái)決定的。
ReadView讀視圖中包含了四個(gè)核心字段,也是讀取數(shù)據(jù)的判斷依據(jù): | 字段 | 含義 | | -------------- | ---------------------------------------------------- | | m_ids | 當(dāng)前活躍的事務(wù)ID集合 | | min_trx_id | 最小活躍事務(wù)ID | | max_trx_id | 預(yù)分配事務(wù)ID,當(dāng)前最大事務(wù)ID+1(因?yàn)槭聞?wù)ID是自增的) | | creator_trx_id | ReadView創(chuàng)建者的事務(wù)ID |
ReadView一共有四種匹配規(guī)則: | 條件 | 能否訪問(wèn) | 說(shuō)明 | | ---------------------------------- | ----------------------------------------- | -------------------------------------------- | | trx_id == creatro_trx_id | 可以訪問(wèn)該版本 | 成立,說(shuō)明數(shù)據(jù)是當(dāng)前這個(gè)事務(wù)更改的。 | | trx_id < min_trx_id | 可以訪問(wèn)該版本 | 成立,說(shuō)明數(shù)據(jù)已經(jīng)提交了。 | | trx_id > max_trx_id | 不可以訪問(wèn)該版本 | 成立,說(shuō)明該事務(wù)是在ReadView生成后才開(kāi)啟的。 | | min_trx_id <= trx_id <= max_trx_id | 如果trx_id不在m_ids中,那么可以訪問(wèn)該版本 | 成立,說(shuō)明數(shù)據(jù)已經(jīng)提交。 |
不同隔離級(jí)別下MVCC分析
READ-COMMITTED隔離級(jí)別
前面有提到過(guò)在READ-COMMITTED隔離級(jí)別下事務(wù)在每次快照讀SQL執(zhí)行時(shí)創(chuàng)建ReadView,每次創(chuàng)建的ReadView的四個(gè)字段對(duì)應(yīng)的值也是不同的,所以在READ-COMMITTED隔離級(jí)別下每次快照讀SQL獲取的數(shù)據(jù)可能也是不同的。
下面通過(guò)一個(gè)READ-COMMITTED隔離級(jí)別下并發(fā)事務(wù)的案例來(lái)詳細(xì)看看:
現(xiàn)有四個(gè)并發(fā)事務(wù)同時(shí)訪問(wèn)一條數(shù)據(jù):
在上述并發(fā)事務(wù)中,事務(wù)5查詢了兩次id為1的數(shù)據(jù),因?yàn)楫?dāng)前的隔離級(jí)別設(shè)置為了READ-COMMITTED,事務(wù)在每次快照讀SQL執(zhí)行時(shí)創(chuàng)建一個(gè)ReadView,每次生成的ReadView中的四個(gè)字段值都不同。那么三次快照讀都會(huì)根據(jù)生成的ReadView中的字段進(jìn)行規(guī)則匹配,從而決定返回的數(shù)據(jù)。接下來(lái)看看流程:
事務(wù)5第一次快照讀解讀
事務(wù)5第一次進(jìn)行查詢時(shí)生成的ReadView以及原數(shù)據(jù)如下圖:
在匹配版本數(shù)據(jù)前,先與表中數(shù)據(jù)進(jìn)行匹配:
該數(shù)據(jù)對(duì)應(yīng)的DB_TRX_ID為3,此時(shí)MVCC就會(huì)通過(guò)ReadView帶著這條數(shù)據(jù)去進(jìn)行規(guī)則匹配:
首先是第一條規(guī)則db_trx_id == creator_trx_id,db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id < min_trx_id,db_trx_id(3)不小于min_trx_id(3)故不成立;
第三條規(guī)則db_trx_id > max_trx_id,db_trx_id(3)小于max_trx_id(6)故不成立;
第四條規(guī)則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(3)在min_trx(3)與max_trx_id(6)之間,但是同時(shí)處于m_ids(3,4,5)集合之中故也不成立。
經(jīng)過(guò)這次匹配,表中最新的數(shù)據(jù)無(wú)法匹配,故要與MVCC版本鏈中最上面的數(shù)據(jù)進(jìn)行規(guī)則匹配
與MVCC版本鏈中最上方的版本進(jìn)行匹配:
第一條規(guī)則db_trx_id(2)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id(2)小于min_trx_id(3),該版本的數(shù)據(jù)滿足匹配規(guī)則中的第二條,說(shuō)明數(shù)據(jù)已經(jīng)提交,此時(shí)匹配將終止并返回這個(gè)版本對(duì)應(yīng)的數(shù)據(jù)。
事務(wù)5第二次快照讀
因?yàn)楫?dāng)前事務(wù)的隔離級(jí)別為READ-COMMITTED(讀已提交),所以在每次快照讀的時(shí)候都會(huì)創(chuàng)建一個(gè)ReadView,所以事務(wù)5第二次進(jìn)行查詢時(shí)生成的ReadView以及原數(shù)據(jù)如下圖:
在匹配版本數(shù)據(jù)前,先與表中數(shù)據(jù)進(jìn)行匹配:
該數(shù)據(jù)對(duì)應(yīng)的DB_TRX_ID為4,此時(shí)MVCC就會(huì)通過(guò)ReadView帶著這條數(shù)據(jù)去進(jìn)行規(guī)則匹配:
首先是第一條規(guī)則db_trx_id == creator_trx_id,db_trx_id(4)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id < min_trx_id,db_trx_id(4)不小于min_trx_id(4)故不成立;
第三條規(guī)則db_trx_id > max_trx_id,db_trx_id(4)小于max_trx_id(6)故不成立;
第四條規(guī)則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(4)在min_trx(4)與max_trx_id(6)之間,但是同時(shí)處于m_ids(4,5)集合之中故也不成立。
經(jīng)過(guò)這次匹配,表中最新的數(shù)據(jù)無(wú)法匹配,故要與MVCC版本鏈中最上面的數(shù)據(jù)進(jìn)行規(guī)則匹配
與MVCC版本鏈中最上方的版本進(jìn)行匹配:
第一條規(guī)則db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id(3)小于min_trx_id(4),該版本的數(shù)據(jù)滿足匹配規(guī)則中的第二條,說(shuō)明數(shù)據(jù)已經(jīng)提交,此時(shí)匹配將終止并返回這個(gè)版本對(duì)應(yīng)的數(shù)據(jù)。
REPEATABLE-READ級(jí)別
現(xiàn)在來(lái)看看REPEATABLE-READ可重復(fù)讀隔離級(jí)別有什么不同的地方。同樣,有四個(gè)并發(fā)事務(wù)同時(shí)訪問(wèn)一條數(shù)據(jù):
在上述并發(fā)事務(wù)中,事務(wù)5查詢了兩次id為1的數(shù)據(jù),因?yàn)楫?dāng)前的隔離級(jí)別設(shè)置為了REPEATABLE-READ,事務(wù)在第一次快照讀SQL執(zhí)行時(shí)創(chuàng)建ReadView,后續(xù)該事務(wù)所有的快照讀都復(fù)用該ReadView。接下來(lái)看看流程:
事務(wù)5第一次快照讀解讀
事務(wù)5第一次進(jìn)行查詢時(shí)生成的ReadView以及原數(shù)據(jù)如下圖:
在匹配版本數(shù)據(jù)前,先與表中數(shù)據(jù)進(jìn)行匹配:
該數(shù)據(jù)對(duì)應(yīng)的DB_TRX_ID為3,此時(shí)MVCC就會(huì)通過(guò)ReadView帶著這條數(shù)據(jù)去進(jìn)行規(guī)則匹配:
首先是第一條規(guī)則db_trx_id == creator_trx_id,db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id < min_trx_id,db_trx_id(3)不小于min_trx_id(3)故不成立;
第三條規(guī)則db_trx_id > max_trx_id,db_trx_id(3)小于max_trx_id(6)故不成立;
第四條規(guī)則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(3)在min_trx(3)與max_trx_id(6)之間,但是同時(shí)處于m_ids(3,4,5)集合之中故也不成立。
經(jīng)過(guò)這次匹配,表中最新的數(shù)據(jù)無(wú)法匹配,故要與MVCC版本鏈中最上面的數(shù)據(jù)進(jìn)行規(guī)則匹配
與MVCC版本鏈中最上方的版本進(jìn)行匹配:
第一條規(guī)則db_trx_id(2)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id(2)小于min_trx_id(3),該版本的數(shù)據(jù)滿足匹配規(guī)則中的第二條,說(shuō)明數(shù)據(jù)已經(jīng)提交,此時(shí)匹配將終止并返回這個(gè)版本對(duì)應(yīng)的數(shù)據(jù)。
事務(wù)5第二次快照讀解讀
因?yàn)楫?dāng)前事務(wù)的隔離級(jí)別為REPEATABLE-READ(可重復(fù)讀),所以第二次快照讀也會(huì)沿用第一次快照讀時(shí)創(chuàng)建的ReadView,如下:
在匹配版本數(shù)據(jù)前,先與表中數(shù)據(jù)進(jìn)行匹配:
該數(shù)據(jù)對(duì)應(yīng)的DB_TRX_ID為4,此時(shí)MVCC就會(huì)通過(guò)ReadView帶著這條數(shù)據(jù)去進(jìn)行規(guī)則匹配:
首先是第一條規(guī)則db_trx_id == creator_trx_id,db_trx_id(4)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id < min_trx_id,db_trx_id(4)不小于min_trx_id(3)故不成立;
第三條規(guī)則db_trx_id > max_trx_id,db_trx_id(4)小于max_trx_id(6)故不成立;
第四條規(guī)則min_trx_id <= db_trx_id <= max_trx_id,db_trx_id(4)在min_trx(4)與max_trx_id(6)之間,但是同時(shí)處于m_ids(4,5)集合之中故也不成立。
經(jīng)過(guò)這次匹配,表中最新的數(shù)據(jù)無(wú)法匹配,故要與MVCC版本鏈中最上面的數(shù)據(jù)進(jìn)行規(guī)則匹配
與MVCC版本鏈中最上方的版本進(jìn)行匹配:
第一條規(guī)則db_trx_id(3)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id(3)不小于min_trx_id(4)故不成立;
第三條規(guī)則db_trx_id小于max_trx_id(6)故不成立;
第四條規(guī)則db_trx_id(3)在min_trx(3)與max_trx_id(6)之間,但是同時(shí)處于m_ids(3,4,5)集合之中故也不成立。
經(jīng)過(guò)第二次匹配,MVCC版本鏈中最上層的數(shù)據(jù)版本也無(wú)法匹配,故要與第二條版本進(jìn)行匹配
與MVCC版本鏈中第二條版本進(jìn)行匹配:
第一條規(guī)則db_trx_id(2)不等于creator_trx_id(5)故不成立;
第二條規(guī)則db_trx_id(2)小于min_trx_id(3),該版本的數(shù)據(jù)滿足匹配規(guī)則中的第二條,說(shuō)明數(shù)據(jù)已經(jīng)提交,此時(shí)匹配將終止并返回這個(gè)版本對(duì)應(yīng)的數(shù)據(jù)。
Enjoy GreatSQL :)
關(guān)于 GreatSQL
GreatSQL是由萬(wàn)里數(shù)據(jù)庫(kù)維護(hù)的MySQL分支,專注于提升MGR可靠性及性能,支持InnoDB并行查詢特性,是適用于金融級(jí)應(yīng)用的MySQL分支版本。
相關(guān)鏈接: GreatSQL社區(qū) Gitee GitHub Bilibili
GreatSQL社區(qū):
歡迎來(lái)GreatSQL社區(qū)發(fā)帖提問(wèn) https://greatsql.cn/
技術(shù)交流群:
微信:掃碼添加GreatSQL社區(qū)助手微信好友,發(fā)送驗(yàn)證信息加群。
總結(jié)
以上是生活随笔為你收集整理的图文结合带你搞懂InnoDB MVCC的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 数据结构代码
- 下一篇: C++ | PaddleOCR GPU版