Mysql InnoDB多版本并發(fā)控制MVCC詳解
一丶為什么需要事務(wù)隔離級別
mysql是一個客戶端/服務(wù)端軟件,對于同一個服務(wù)器來說,可以有多個客戶端進(jìn)行連接,每一個客戶端進(jìn)行連接之后就形成一個會話,每一個客戶端都可以在自己的會話中向服務(wù)器發(fā)出請求語句,一個請求語句可能是某一個事務(wù)的一部分,服務(wù)器可以同時處理多個事務(wù)。
如果事務(wù)時一個接著一個進(jìn)行,那么下一個事務(wù)是在上一個事務(wù)的一致性前提下進(jìn)行的,就沒用一致性的問題,但是事務(wù)是并發(fā)進(jìn)行且可能訪問到相同的數(shù)據(jù)這時候就會出現(xiàn)如下問題
可以看到AB最開始總和13元,最后AB總和18元,銀行血虧五元,這顯然違背了一致性——錢的總量不變。這就是并發(fā)情況下兩個事務(wù)的影響,所以需要事務(wù)隔離
讓事務(wù)隔離的進(jìn)行,互不干涉。
1.實現(xiàn)事務(wù)隔離的方式:串行執(zhí)行
最簡單直接的方式,同一時間只能有一個事務(wù)運行,這樣必然不會有上述不一致的情況,但是大大降低了吞吐率并增加了事務(wù)的等待時間
2.實現(xiàn)事務(wù)隔離的方式:可串行執(zhí)行
并發(fā)事務(wù)之所以出現(xiàn)不一致的情況,就是由于多個事務(wù)訪問相同的數(shù)據(jù),需要實現(xiàn)多個事務(wù)在訪問相同數(shù)據(jù)的時候進(jìn)行限制,比方說上圖中事務(wù)2想訪問A賬戶的值需要等待事務(wù)提交事務(wù)之后,這樣可以讓并發(fā)事務(wù)的執(zhí)行如同串行執(zhí)行的效果一樣。
二丶并發(fā)事務(wù)執(zhí)行的問題:臟寫,臟讀,不可重復(fù)讀,幻讀
1.臟寫
一個事務(wù)修改了另外一個未提交事務(wù)修改過的數(shù)據(jù)
臟寫導(dǎo)致一致性無法保證
上圖事務(wù)A和事務(wù)B都更新紫色數(shù)據(jù),其中事務(wù)A首先更新為A,然后事務(wù)B過來更新為B,這時候事務(wù)A回滾后更新為Null,事務(wù) B 明明正常寫了一行數(shù)據(jù),但是寫完之后發(fā)現(xiàn)值變了,有點丟失更新的意思。(比如A表示余額,這時候在將余額A判斷是否足以支付,判斷得到可以,事務(wù)B執(zhí)行扣費寫入A-5,商家收到5元,結(jié)果這時候回滾了,A變成Null,事務(wù)A中轉(zhuǎn)錢的一方錢變?yōu)锳,錢的總額變?yōu)锳+5了)
臟寫導(dǎo)致原子性受到破壞
假如上述的事務(wù)B還操作了另外的數(shù)據(jù),比如插入一條數(shù)據(jù)C,并且更改為B寫入C是在一個事務(wù)下面的,需要具備原子性,但是臟寫讓B的更改需要部分回滾為Null,這樣插入C和更改B就不具備原子性(比如A表示余額,這時候在將余額A判斷是否足以支付,判斷得到可以,事務(wù)B執(zhí)行扣費寫入A-5,商家收到5元,結(jié)果這時候回滾了,A變成Null,這時候部分回滾,商家的5元沒用回滾,商家的庫存也沒用回滾,原子性被破壞)
2.臟讀
如果一個事務(wù)讀取到另外一個事務(wù)未提交的數(shù)據(jù),意味著發(fā)生了臟讀
比如事務(wù)A先寫數(shù)據(jù)A,然后事務(wù)B督導(dǎo)數(shù)據(jù)A后在內(nèi)存中使用A進(jìn)行一系列操作(比如A表示余額,這時候在將余額A判斷是否足以支付,判斷得到可以)但是事務(wù)A這時候回滾了,事務(wù)B再次讀取數(shù)據(jù)發(fā)現(xiàn)為null,這就是臟讀。
臟讀可能引發(fā)一致性的問題:比如事務(wù)操作時修改x和y的值,并且二者總是相等的,A修改x為1,還沒來得及修改y也沒用提交事務(wù),這時候事務(wù)B讀取x=1,y=0,二者不等,事務(wù)B讀取到了數(shù)據(jù)庫不一致的狀態(tài),讀取到未提交事務(wù)的值
3.不可重復(fù)讀
假如一個事務(wù)修改了另外一個事務(wù)未提交的數(shù)據(jù),意味發(fā)生了不可重復(fù)讀
比如事務(wù)A第一次讀取到值為A,接著事務(wù)B修改為B,并且提交了事務(wù)B,然后事務(wù)A再次讀取得到的數(shù)據(jù)是B,同一行數(shù)據(jù)多次讀取值并不相同,這稱作不可重復(fù)讀。它是指在同一個事務(wù)里面查詢同一行數(shù)據(jù),每次查到的數(shù)據(jù)都不一樣。和臟讀區(qū)別在于臟讀是由于別的事務(wù)回滾導(dǎo)致,而不可重復(fù)讀讀到的其實是已經(jīng)提交的數(shù)據(jù)。
事務(wù)A讀到事務(wù)B提交后的數(shù)據(jù)似乎很合理,但是我們想象這樣一種場景:你有一個流水表和用戶余額,其中記錄用戶每天的流水,你在月初0點的時候核對流水和庫存,但是流水很多,你的程序選擇一個一個用戶的進(jìn)行核對,核對用戶甲,甲沒做任何消費,但是當(dāng)你核對B的時候,你將B的流水load到內(nèi)存中,但是B這時候(0點30分,這一筆數(shù)據(jù)新的一個余額)進(jìn)行了扣除余額的操作,導(dǎo)致B余額和流水對不上了。
4.幻讀
如果一個事務(wù)A先根據(jù)沒用搜索條件查詢到一些記錄,在該事務(wù)未提交前,另外一個事務(wù)寫入(delete,update,insert)了符合搜索條件的記錄,這時候事務(wù)A再次讀取,發(fā)現(xiàn)數(shù)據(jù)條數(shù)和第一次讀取的不同,如同出現(xiàn)了幻覺,稱之為幻讀
事務(wù)A讀到事務(wù)B提交后的數(shù)據(jù)似乎很合理,但是我們想象這樣一種場景:你有一個需求將會公司的男性員工了女性員工查詢進(jìn)行展示,你先查詢了總數(shù)為100人,然后查詢男性的總數(shù)50人,后查詢女性人數(shù)準(zhǔn)備在頁面展示共100人,其中男50人,女50人
,結(jié)果這是管理信息的人發(fā)現(xiàn)有一位員工性別錯誤錄入了,將其從男修改為女,這時候你讀取事務(wù)就是女51人了,你在主頁顯示了共100人,其中男50人,女51人
三丶隔離級別
1.Read UnCommitted 讀未提交
在此隔離級別下,會發(fā)生臟讀,不可重復(fù)讀,和幻讀
2.Read Committed 讀已提交
在此隔離級別下,會發(fā)生不可重復(fù)讀,和幻讀
3.Repeatable Read 可重復(fù)讀
在此隔離級別下,可能發(fā)生幻讀
4.Serializable 可串行化
在此隔離級別下,不會發(fā)生臟讀,不可重復(fù)讀,和幻讀
其中臟寫是對一致性影響最嚴(yán)重的,無論是何種隔離級別,都不允許臟寫發(fā)生,innodb使用鎖保證不會出現(xiàn)臟寫現(xiàn)象,第一個事務(wù)更新某條記錄的時候,會給這條記錄加鎖,另外一個事務(wù)在此更新的時候,需要等待第一個事務(wù)提交釋放鎖后更新。隔離級別越高,其并發(fā)能力越低。
四丶Mysql設(shè)置隔離級別
默認(rèn)隔離級別可重復(fù)讀
1.設(shè)置全局隔離級別
SET GLOBAL TRANSACTION ISOLATION LEVEL 期望的隔離級別(可選READ UNCOMMITED,READ COMMITED,REPEATABLE READ,SERIALIZABLE)
,此命令只對執(zhí)行語句后新產(chǎn)生的會話有效,對當(dāng)前已經(jīng)存在的會話無效
2.設(shè)置會話隔離級別
SET SESSION TRANSACTION ISOLATION LEVEL 期望的隔離級別(可選READ UNCOMMITED,READ COMMITED,REPEATABLE READ,SERIALIZABLE)
,對當(dāng)前會話后續(xù)事務(wù)有效,該語句可以在已開啟的事務(wù)中執(zhí)行,但是不會影響當(dāng)前正在執(zhí)行的事務(wù),如果在事務(wù)之間執(zhí)行,只會對后續(xù)的事務(wù)有效
3.設(shè)置下一個事務(wù)的隔離級別
SET TRANSACTION ISOLATION LEVEL 期望的隔離級別(可選READ UNCOMMITED,READ COMMITED,REPEATABLE READ,SERIALIZABLE)
只對當(dāng)前會話的下一個即將開啟的事務(wù)有效,下一個事務(wù)執(zhí)行完后,后續(xù)事務(wù)將恢復(fù)到之前的隔離級別,該語句不能再已經(jīng)開啟的事務(wù)中執(zhí)行,否則會報錯。
4.指定服務(wù)器的隔離級別
在啟動的時候使用--transaction-isolation=xxx
即可執(zhí)行默認(rèn)隔離級別
五丶MVCC原理
下面討論記錄對當(dāng)前事務(wù)是否可見都是基于當(dāng)前事務(wù)中執(zhí)行的查詢是快照讀(普通查詢),對于當(dāng)前讀(select xxx for update,select xxx lock in share mode)是不通用的
1.版本鏈
對于InnoDB存儲引擎來說,其聚簇索引記錄中包含兩個隱藏列:
trx_id
:一個事務(wù)每次對聚簇索引記錄做出改動的時候,都會把該事務(wù)的事務(wù)id復(fù)制給此列 roll_point
:每次對某條聚簇索引記錄進(jìn)行改動的時,都會把舊的版本寫入到undo 日志中,此列相當(dāng)于一個指針,指向修改前的信息
每次修改都會形成Undo 日志,所有版本的數(shù)據(jù)會通過roll_point
串聯(lián)成一個鏈表,稱之為版本鏈,頭節(jié)點是當(dāng)前記錄的最新值。利用版本鏈控制多個并發(fā)事務(wù)訪問相同記錄時的行為稱為MVCC多版本并發(fā)控制。
其實在undo日志中,只記錄被更新列的信息,而不是記錄全部的信息,對于沒有記錄的列,會通過版本鏈找少一個版本中的對應(yīng)列的信息,直到找到聚簇索引葉子節(jié)點中的內(nèi)容
2.Read View
對于使用Read Uncommitted隔離級別的事務(wù),可以讀取到?jīng)]提交的數(shù)據(jù),那么直接讀取最新的版本即可。對于Serializable隔離級別,innodb直接通過加鎖來訪問記錄。對于read committed 和 repeatable read
隔離級別的事務(wù),都必須保證督導(dǎo)的數(shù)據(jù)是已經(jīng)提交事務(wù)修改過的記錄,那么如何判斷版本鏈中的哪個版本的數(shù)據(jù)是當(dāng)前事務(wù)可見的昵?
innodb 使用的Read View
2.1 read view 的結(jié)構(gòu)
- m_ids:在生成read view時,當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)id列表
- min_trx_id:生成read view時,當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小事務(wù)id,也就是m_ids中的最小值
- max_trx_id:生成read view時,系統(tǒng)應(yīng)該分配給下一個事務(wù)的事務(wù)id值
- creator_trx_id:生成該read view的事務(wù)的事務(wù)id
2.2 read view
- 如果被訪問版本的
trx_id
和creator_trx_id
相同,意味著當(dāng)前事務(wù)在訪問自己修改的記錄,自然可見 - 如果訪問版本的
trx_id
屬性值小于read view中的min_trx_id
表明此版本是生成read view之前已經(jīng)提交的事務(wù),那么自然可見 - 如果訪問版本的
trx_id
,大于等于read view中的max_trx_id
說明,當(dāng)前版本數(shù)據(jù)是生成read view后開啟事務(wù)產(chǎn)生的,那么自然不可見 - 如果訪問版本的
trx_id
介于min_trx_id
和max_trx_id
之間,需要判斷trx_id
是否位于m_ids
列表中,如果在說明創(chuàng)建read view時生成該版本的事務(wù)還是活躍的,那么該版本,不可被訪問,如果不在說明創(chuàng)建read view 時生成該版本的事務(wù)已經(jīng)提交,可以被訪問到
如果某個版本數(shù)據(jù)對當(dāng)前事務(wù)不可見那么需要一直順著版本鏈找上一個版本的數(shù)據(jù),并通過上述步驟判斷是否可見,直到找到可見的版本,如果一直找不到說明該條記錄對當(dāng)前事務(wù)不可見,查詢結(jié)果將不包含該記錄。
2.3 Read Committed和 Repeatable Read的不同
Read Committed——每次讀取數(shù)據(jù)前都生成一個Read View
這樣可以保證生成Read view 中的m_ids是實時活躍事務(wù)id集合,也許第一次讀取的時候事務(wù)A沒提交,其id位于m_ids中,但是第二次讀取的時候事務(wù)A提交了,事務(wù)A將不位于m_ids中,這樣在第二次讀取的時候,通過m_ids判斷事務(wù)A是否提交的時候,可以得到事務(wù)A已經(jīng)提交了,然后讓事務(wù)A版本產(chǎn)生的數(shù)據(jù)可見(見2.2.4中的內(nèi)容)。
Repeatable Read——如果使用begin開啟事務(wù)那么在第一次查詢的時候生成Read view,如果使用start transaction with consistent snapshot
那么執(zhí)行的時候就會生成read view
這樣可以保證當(dāng)前事務(wù)從頭到尾都是read view中記錄的內(nèi)容是一致的,第一次讀取的時候事務(wù)A沒有提交,那么不可見,但是第二次讀取的時候事務(wù)A提交了,但是read view的m_ids
和max_trx_id
可以判斷事務(wù)A不可見,比如事務(wù)A事務(wù)id小于max_trx_id意味著生成read view是事務(wù)A啟動但是沒提交,即使第二次讀事務(wù)A提交了,但是m_ids
中還是包含事務(wù)A,那么不可見。如果事務(wù)A事務(wù)id大于max_trx_id,那么自然第二次還是大于max_trx_id,也是不可見的,從而實現(xiàn)了可重復(fù)讀。
2.4 二級索引與MVCC
上面我們提到,innodb聚簇索引組織的記錄才具備trx_id
和roll_point
,那么我們使用二級索引進(jìn)行查詢的時候,如何判斷數(shù)據(jù)是否可見昵?
- 二級索引頁面的page header中存在page_max_trx_id屬性,每當(dāng)有事務(wù)對其中的記錄進(jìn)行增刪改查操作的時候,如果事務(wù)的事務(wù)id,大于page_max_trx_id,那么會更新page_max_trx_id屬性值為其事務(wù)id,這意味著page_max_trx_id記錄了修改該二級索引頁面最大的事務(wù)id是多少。當(dāng)select通過二級索引首先看下對于read view的min_trx_id是否大于該頁面的page_max_trx_id,如果大于那么頁面中所有記錄都對該read view可見,否則就進(jìn)行下面的第二步
- 利用二級索引中的主鍵值,進(jìn)行回標(biāo),得到對應(yīng)的聚簇索引記錄然后進(jìn)行回表,然后通過2.2中步驟拿到第一個可見版本的數(shù)據(jù),然后比對此紀(jì)錄和通過二級索引查詢得到記錄的值是否相同,如果相同那么發(fā)送給客戶端,否則跳過該記錄。
到此這篇關(guān)于Mysql InnoDB多版本并發(fā)控制MVCC詳解的文章就介紹到這了,更多相關(guān)Mysql InnoDB多版本并發(fā)控制MVCC內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Using filesort和Using temporary 為什么這么慢
本文主要介紹了Using filesort和Using temporary為什么這么慢,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02mariadb集群搭建---Galera Cluster+ProxySQL教程
這篇文章主要介紹了mariadb集群搭建---Galera Cluster+ProxySQL教程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03mysqldump命令導(dǎo)入導(dǎo)出數(shù)據(jù)庫方法與實例匯總
這篇文章主要介紹了mysqldump命令導(dǎo)入導(dǎo)出數(shù)據(jù)庫方法與實例匯總的相關(guān)資料,需要的朋友可以參考下2015-10-10關(guān)于mysql中string和number的轉(zhuǎn)換問題
這篇文章主要介紹了關(guān)于mysql中string和number的轉(zhuǎn)換問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06Mysql聯(lián)合查詢UNION和UNION ALL的使用介紹
本文詳細(xì)介紹了Mysql的聯(lián)合查詢命令UNION和UNION ALL,總結(jié)了使用語法和注意事項,以及學(xué)習(xí)例子和項目例子,需要的朋友可以參考下2014-04-04MySQL如何利用存儲過程快速生成100萬條數(shù)據(jù)詳解
在MySQL數(shù)據(jù)庫中,如果要插入上百萬級的記錄,用普通的insertinto來操作非常不現(xiàn)實,速度慢人力成本高,這篇文章主要給大家介紹了關(guān)于MySQL如何利用存儲過程快速生成100萬條數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2021-08-08CentOS6.7 mysql5.6.33修改數(shù)據(jù)文件位置的方法
mysql存放的數(shù)據(jù)文件,分區(qū)容量較小,目前已經(jīng)滿,導(dǎo)致mysql連接不上,怎么解決呢?下面小編給大家分享CentOS6.7 mysql5.6.33修改數(shù)據(jù)文件位置的方法,一起看看吧2017-06-06