认真学习MySQL中的MVCC机制
什么是MVCC?MVCC(Multiversion Concurrency Control),多版本并發(fā)控制。顧名思義,MVCC是通過數(shù)據(jù)行的多個(gè)版本管理來實(shí)現(xiàn)數(shù)據(jù)庫的并發(fā)控制。這項(xiàng)技術(shù)使得在InnoDB的事務(wù)隔離級(jí)別下執(zhí)行一致性操作有了保證。換言之,就是為了查詢一些正在被另一個(gè)事務(wù)更新的行,并且可以看到它們被更新之前的值,這樣在做查詢的時(shí)候就不用等待另一個(gè)事務(wù)釋放鎖。
MVCC的實(shí)現(xiàn)原理依賴于:隱藏字段、undo log 、Read View。多版本通過Undo log體現(xiàn),并發(fā)控制通過Read View體現(xiàn)。
MVCC沒有正式的標(biāo)準(zhǔn),在不同的DBMS中MVCC的實(shí)現(xiàn)方式可能是不同的,也不是普遍使用的。本文學(xué)習(xí)InnoDB中MVCC的實(shí)現(xiàn)機(jī)制(MySQL其他的存儲(chǔ)引擎并不支持它)。
【1】快照度和當(dāng)前度
MVCC在MySQL InnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫并發(fā)性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時(shí),也能做到不加鎖,非阻塞并發(fā)讀,而這個(gè)讀指的就是快照讀,而非當(dāng)前讀。當(dāng)前讀實(shí)際上是一種加鎖的操作,是悲觀鎖的實(shí)現(xiàn)。而MVCC本質(zhì)是采用樂觀鎖思想的一種方式。
① 快照讀
快照讀又叫一致性讀,讀取的是快照數(shù)據(jù)。不加鎖的簡單的select都屬于快照讀,即不加鎖的非阻塞讀。比如下例:
select * from player where ...之所以出現(xiàn)快照讀的情況,是基于提高并發(fā)性能的考慮,快照讀的實(shí)現(xiàn)是基于MVCC,它在很多情況下,避免了加鎖操作,降低了開銷。
既然是基于多版本,那么快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本,而有可能是之前的歷史版本。
快照讀的前提是隔離級(jí)別不是串行級(jí)別,串行級(jí)別下的快照讀會(huì)退化成當(dāng)前讀。
② 當(dāng)前讀
當(dāng)前讀讀取的是記錄的最新版本(最新數(shù)據(jù),而不是歷史版本的數(shù)據(jù)),讀取時(shí)還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄,會(huì)對(duì)讀取的記錄進(jìn)行加鎖。加鎖的select,或者對(duì)數(shù)據(jù)進(jìn)行增刪改都會(huì)進(jìn)行當(dāng)前讀。
示例:
select * from student lock in share mode; # 共享鎖select * from student for update ;#排他鎖insert into student values ... #排他鎖delete from student where ... #排他鎖update student set ... #排他鎖【2】MVCC
① 回顧隔離級(jí)別
我們知道事務(wù)有4個(gè)隔離級(jí)別,可能存在三種并發(fā)問題:
在MySQL中,默認(rèn)的隔離級(jí)別是可重復(fù)讀,可以解決臟讀和不可重復(fù)讀的問題。如果僅從定義的角度來看,它并不能解決幻讀問題。如果我們想要解決幻讀問題,就需要采用串行化的方式,也就是將隔離級(jí)別提升到最高,但這樣一來就會(huì)大幅度降低數(shù)據(jù)庫的事務(wù)并發(fā)能力。
MVCC可以不采用鎖機(jī)制,而是通過樂觀鎖的方式來解決不可重復(fù)讀和幻讀問題。它可以在大多數(shù)情況下替代行鎖,降低系統(tǒng)的開銷。
② 隱藏字段和Undo Log版本鏈
回顧一下undo日志的版本鏈,對(duì)于使用InnoDB存儲(chǔ)引擎的表來說,它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列(其實(shí)還可能有一個(gè)隱藏的ID-row_id是在沒有自定義主鍵以及Unique鍵的情況下才會(huì)存在的)。
- trx_id:每次一個(gè)事務(wù)對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的事務(wù)id賦值給trx_id 隱藏列。
- roll_pointer:每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊的版本寫入到undo日志中,然后這個(gè)隱藏列就相當(dāng)于一個(gè)指針,可以通過它來找到該記錄修改前的信息。
舉例:student表數(shù)據(jù)如下
假設(shè)插入該記錄的事務(wù)id為8,那么此刻該條記錄的示意圖如下所示:
insert undo只在事務(wù)回滾時(shí)起作用,當(dāng)事務(wù)提交后,該類型的undo日志就沒用了,它占用的undo log segment也會(huì)被系統(tǒng)回收(也就是該undo 日志占用的undo頁面鏈表要么被重用,要么被釋放)。
假設(shè)之后兩個(gè)事務(wù)id分別為10/20的事務(wù)對(duì)這條記錄進(jìn)行update操作,操作流程如下:
| 1 | begin; | |
| 2 | begin; | |
| 3 | update student set name=‘李四’ where id=1 | |
| 4 | update student set name=‘王五’ where id=1 | |
| 5 | commit; | |
| 6 | update student set name=‘錢七’ where id=1 | |
| 7 | update student set name=‘宋八’ where id=1 | |
| 8 | commit; |
能不能在兩個(gè)事務(wù)中交叉更新同一條記錄呢?不能!這不就是一個(gè)事務(wù)修改了另一個(gè)未提交事務(wù)修改過的數(shù)據(jù)嗎?臟寫!
InnoDB使用鎖來保證不會(huì)有臟寫情況的發(fā)生,也就是在第一個(gè)事務(wù)更新了某條記錄后,就會(huì)給這條記錄加鎖,另一個(gè)事務(wù)再次更新時(shí)就需要等待第一個(gè)事務(wù)提交了,把鎖釋放之后才可以繼續(xù)更新。
每次對(duì)記錄進(jìn)行改動(dòng),都會(huì)記錄一條undo日志,每條undo日志也都有一個(gè)roll_pointer屬性(insert操作對(duì)應(yīng)的undo日志沒有該屬性,因?yàn)樵撚涗洸]有更早的版本),可以將這些undo日志都連起來,串成一個(gè)鏈表。
對(duì)該記錄每次更新后,都會(huì)將舊值放到一條undo日志中,就算是該記錄的一個(gè)舊版本。隨著更新次數(shù)的增多,所有的版本都會(huì)被roll_pointer屬性連接成一個(gè)鏈表,我們把這個(gè)鏈表稱之為版本鏈,版本鏈的頭節(jié)點(diǎn)就是當(dāng)前記錄最新的值。
版本鏈會(huì)無限增長嗎?不會(huì)!如果undo log一直不刪除,則會(huì)通過當(dāng)前記錄的回滾指針回溯到該行創(chuàng)建時(shí)的初始內(nèi)容。所幸的是在Innodb中存在purge線程,它會(huì)查詢那些比現(xiàn)在最老的活動(dòng)事務(wù)還早的undo log,并刪除它們,從而保證undo log文件不至于無限增長。
每個(gè)版本中還包含生成該版本時(shí)對(duì)應(yīng)的事務(wù)id。
【3】MVCC實(shí)現(xiàn)原理之ReadView
MVCC的實(shí)現(xiàn)依賴于:隱藏字段、Undo Log、Read View。
① 什么是ReadView
在MVCC機(jī)制中,多個(gè)事務(wù)對(duì)同一個(gè)行記錄進(jìn)行更新會(huì)產(chǎn)生多個(gè)歷史快照,這些歷史快照保存在undo log里,如果一個(gè)事務(wù)想要查詢查詢這個(gè)行記錄,需要讀取哪個(gè)版本的歷史記錄呢?這時(shí)就需要用到ReadView了,它幫我們解決了行的可見性問題。
ReadView就是事務(wù)在使用MVCC機(jī)制進(jìn)行快照讀操作時(shí)產(chǎn)生的讀視圖。當(dāng)事務(wù)啟動(dòng)時(shí),會(huì)生成數(shù)據(jù)庫系統(tǒng)當(dāng)前的一個(gè)快照,InnoDB為每個(gè)事務(wù)構(gòu)造了一個(gè)數(shù)組,用來記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)的ID("活躍"指的就是,啟動(dòng)了但還沒提交)。
② 設(shè)計(jì)思路
使用READ UNCOMMITTED隔離級(jí)別的事務(wù),由于可以讀到未提交事務(wù)修改過的記錄,所以直接讀取記錄的最新版本就好了。
使用 SERIALIZABLE 隔離級(jí)別的事務(wù),InnoDB規(guī)定使用加鎖的方式來訪問記錄。
使用 READ COMMITTED 和 REPEATABLE READ隔離級(jí)別的事務(wù),都必須保證讀到 已經(jīng)提交了的 事務(wù)修改過的記錄。假設(shè)另一個(gè)事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的。核心問題就是需要判斷一下版本鏈中的哪個(gè)版本是當(dāng)前事務(wù)可見的,這是ReadView要解決的主要問題。
這個(gè)ReadView中主要包含4個(gè)比較重要的內(nèi)容,分別如下:
- creator_trx_id,創(chuàng)建這個(gè)ReadView的事務(wù)ID。說明:只有在對(duì)表中的記錄做改動(dòng)時(shí)(執(zhí)行insert、delete、update這些語句時(shí))才會(huì)為事務(wù)分配事務(wù)id,否則在一個(gè)只讀事務(wù)中的事務(wù)id值都默認(rèn)為0。
- trx_ids,表示在生成ReadView 時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)的事務(wù)id列表。
- up_limit_id,活躍的事務(wù)中最小的事務(wù)id。
- low_limit_id,表示生成ReadView時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的id值。low_limit_id 是系統(tǒng)最大的事務(wù)id值,這里要注意是系統(tǒng)中的事務(wù)id,需要區(qū)別于正在活躍的事務(wù)Id。
注意,low_limit_id 并不是trx_ids中的最大值。事務(wù)id是遞增分配的,比如現(xiàn)在有id為1,2,3這三個(gè)事務(wù),之后id為3的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成ReadView時(shí),trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
舉例 trx_ids為trx2、trx3、trx5和trx8的集合,系統(tǒng)的最大事務(wù)id(low_limit_id)為trx8+1(如果之前沒有其他的新增事務(wù)),活躍的最小事務(wù)ID(up_limit_id)為trx2。
③ ReadView的規(guī)則
有了這個(gè)ReadView,這樣在訪問某條記錄時(shí),只需要按照下邊的步驟判斷記錄的某個(gè)版本是否可見。
- 如果被訪問版本的trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當(dāng)前事務(wù)在訪問它自己修改過的記錄,所以該版本可以被當(dāng)前事務(wù)訪問。
- 如果被訪問版本的trx_id屬性值小于ReadView中的 up_limit_id 值,標(biāo)明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問。
- 如果被訪問版本的trx_id 屬性值大于或等于ReadView中的 low_limit_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟,所以該版本不可以被當(dāng)前事務(wù)訪問。
- 如果被訪問版本的trx_id屬性值在ReadView的up_limit_id 和low_limit_id之間,那就需要判斷一下trx_id屬性值是不是在trx_ids列表中。
- 如果在,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問
- 如果不在,說明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問
④ MVCC整體操作流程
了解了這些概念之后,我們來看下當(dāng)查詢一條記錄的時(shí)候,系統(tǒng)如何通過MVCC找到它。
如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見的話,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性。以此類推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見的話,那么就意味著該條記錄對(duì)該事務(wù)完全不可見,查詢結(jié)果就不包含該記錄。
InnoDB中,MVCC是通過Undo Log + ReadView進(jìn)行數(shù)據(jù)讀取,Undo Log保存了歷史快照,而ReadView規(guī)則幫我們判斷當(dāng)前版本的數(shù)據(jù)是否可見。
在隔離級(jí)別為讀已提交(Read Commit)時(shí),一個(gè)事務(wù)中的每一次select查詢都會(huì)重新獲取一次Read View。
| begin; | |
| select * from student where id>2; | 獲取一次ReadView |
| … | |
| select * from student where id>2 | 獲取一次ReadView |
| commit; |
注意,此時(shí)同樣的查詢語句都會(huì)重新獲取一次ReadView,這時(shí)如果ReadView不同,就可能產(chǎn)生不可重復(fù)讀或者幻讀的情況。
當(dāng)隔離級(jí)別為可重復(fù)讀的時(shí)候,就避免了不可重復(fù)讀,這時(shí)因?yàn)橐粋€(gè)事務(wù)只在第一次select的時(shí)候會(huì)獲取一次ReadView,而后面所有的select都會(huì)復(fù)用這個(gè)ReadView,如下所示:
| begin; | |
| select * from student where id>2; | 獲取一次ReadView |
| … | |
| select * from student where id>2 | 復(fù)用ReadView |
| commit; |
【4】流程實(shí)例分析
假設(shè)現(xiàn)在student表中只有一條由事務(wù)id為8的事務(wù)插入的一條記錄:
MVCC只能在READ COMMITTED 和 REPEATABLE READ兩個(gè)隔離級(jí)別下工作。接下來看一下 READ COMMITTED 和 REPEATABLE READ所謂的生成ReadView的時(shí)機(jī)不同到底不同在哪里。
① READ COMMITTED隔離級(jí)別下
READ COMMITTED:每次讀取數(shù)據(jù)前都生成一個(gè)ReadView。
現(xiàn)在有兩個(gè)事務(wù)id分別為10/20的事務(wù)在執(zhí)行(事務(wù)的ID是由系統(tǒng)遞增分配的,如果是增刪改行為系統(tǒng)會(huì)分配事務(wù)ID,如果是查詢行為,那么事務(wù)ID為0):
# Transaction 10 begin; update student set name='李四' where id=1; update student set name='王五' where id=1;#Transaction 20 begin; # 更新了一些別的表的記錄說明:事務(wù)執(zhí)行過程中,只有在第一次真正修改記錄時(shí)(比如使用insert、update、delete語句),才會(huì)被分配一個(gè)單獨(dú)的事務(wù)id,這個(gè)事務(wù)id是遞增的。所以我們才在事務(wù)2中更新一些別的表的記錄,目的是讓它分配事務(wù)id。
此刻,表student中id為1的記錄得到的版本鏈表如下所示:
假設(shè)現(xiàn)在有一個(gè)使用 READ COMMITTED隔離級(jí)別的事務(wù)開始執(zhí)行:
# 使用 READ COMMITTED 隔離級(jí)別的事務(wù) begin;# select1: Transaction 10 20 未提交 select * from student where id=1;# 得到的列name的值為 張三這個(gè)select1的執(zhí)行過程如下:
- 1.在執(zhí)行select語句時(shí)會(huì)先生成一個(gè)ReadView,ReadView的 trx_ids 列表的內(nèi)容就是[10,20],up_limit_id 為10,low_limit_id 為21,creator_trx_id 為0。
- 2.從版本 鏈中挑選可見的記錄,從圖中看出,最新版本的列name的內(nèi)容是“王五”,該版本的trx_id 值為10,在trx_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。
- 3.下一個(gè)版本的列name的內(nèi)容是“李四”,該版本的trx_id值也為10,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
- 4.下一個(gè)版本的name的內(nèi)容是“張三”,該版本的trx_id值為8,小于ReadView中的up_limit_id 值 10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為“張三”的記錄。
之后,我們把事務(wù)id為10的事務(wù)提交一下:
# Transaction 10 begin;update student set name='李四' where id=1; update student set name='王五' where id=1;commit;然后再到事務(wù)id為20的事務(wù)中更新一下表student中id為1的記錄:
# Transaction 20 begin;#更新了一些別的表的記錄 ... update student set name='錢七' where id=1; update student set name='宋八' where id=1;此刻,表student中id為1的記錄的版本鏈就長這樣:
然后再到剛才使用 READ COMMITTED 隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id為1的記錄,如下:
這個(gè)select2的執(zhí)行過程如下:
- 1.在執(zhí)行select語句時(shí)又會(huì)單獨(dú)生成一個(gè)ReadView,該ReadView的trx_ids 列表的內(nèi)容就是[20], up_limit_id 為20,low_limit_id 為21,creator_trx_id 為0.
- 2.從版本鏈中挑選可見的記錄,從圖中看出,最新版本的列name的內(nèi)容是“宋八”,該版本的trx_id 值為20,在trx_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。
- 3.下一個(gè)版本的列name的內(nèi)容是“錢七”,該版本的trx_id 值為20,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
- 4.下一個(gè)版本的列name的內(nèi)容是“王五”,該版本的trx_id值為10,小于ReadView中的up_limit_id 值20,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為“王五”的記錄。
以此類推,如果之后事務(wù)id為20的記錄也提交了,再次在使用READ COMMITTED隔離級(jí)別的事務(wù)中查詢表student 中id值為1的記錄時(shí),得到的結(jié)果就是“宋八”了,具體流程我們就不分析了。
強(qiáng)調(diào) : 使用READ COMMITTED隔離級(jí)別的事務(wù)在每次查詢開始時(shí)都會(huì)生成一個(gè)獨(dú)立的ReadView。
② REPEATABLE READ隔離級(jí)別下
使用 REPEATABLE READ 隔離級(jí)別的事務(wù)來說,只會(huì)在第一次執(zhí)行查詢語句時(shí)生成一個(gè)ReadView,之后的查詢就不會(huì)重復(fù)生成了。
現(xiàn)在有兩個(gè)事務(wù)id分別為10/20的事務(wù)在執(zhí)行:
# Transaction 10 begin; update student set name='李四' where id=1; update student set name='王五' where id=1;#Transaction 20 begin; # 更新了一些別的表的記錄此刻,表student中id為1的記錄得到的版本鏈表如下所示:
假設(shè)現(xiàn)在有一個(gè)使用 REPEATABLE READ隔離級(jí)別的事務(wù)開始執(zhí)行:
這個(gè)select1的執(zhí)行過程如下:
- 1.在執(zhí)行select語句時(shí)會(huì)先生成一個(gè)ReadView,ReadView的 trx_ids 列表的內(nèi)容就是[10,20],up_limit_id 為10,low_limit_id 為21,creator_trx_id 為0。
- 2.從版本 鏈中挑選可見的記錄,從圖中看出,最新版本的列name的內(nèi)容是“王五”,該版本的trx_id 值為10,在trx_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。
- 3.下一個(gè)版本的列name的內(nèi)容是“李四”,該版本的trx_id值也為10,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
- 4.下一個(gè)版本的name的內(nèi)容是“張三”,該版本的trx_id值為8,小于ReadView中的up_limit_id 值 10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為“張三”的記錄。
之后,我們把事務(wù)id為10的事務(wù)提交一下:
# Transaction 10 begin;update student set name='李四' where id=1; update student set name='王五' where id=1;commit;然后再到事務(wù)id為20的事務(wù)中更新一下表student中id為1的記錄:
# Transaction 20 begin;#更新了一些別的表的記錄 ... update student set name='錢七' where id=1; update student set name='宋八' where id=1;此刻,表student中id為1的記錄的版本鏈就長這樣:
然后再到剛才使用 REPEATABLE READ 隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id為1的記錄,如下:
# 使用 REPEATABLE READ 隔離級(jí)別的事務(wù) begin;# select1: Transaction 10 20 未提交 select * from student where id=1;# 得到的列name的值為 張三# SELECT2: Transaction 10提交,Transaction 20 未提交 select * from student where id=1; # 得到的列name的值 仍為 張三select2的執(zhí)行過程如下:
- 1.在執(zhí)行select語句時(shí)會(huì)復(fù)用原先的ReadView(因?yàn)楫?dāng)前事務(wù)的隔離級(jí)別為 REPEATABLE READ,而之前在執(zhí)行select1時(shí)已經(jīng)生成過了ReadView),ReadView的 trx_ids 列表的內(nèi)容就是[10,20],up_limit_id 為10,low_limit_id 為21,creator_trx_id 為0。
- 2.從版本 鏈中挑選可見的記錄,從圖中看出,最新版本的列name的內(nèi)容是“宋八”,該版本的trx_id 值為20,在trx_ids列表內(nèi),所以不符合可見性要求,根據(jù)roll_pointer跳到下一個(gè)版本。
- 3.下一個(gè)版本的列name的內(nèi)容是“錢七”,該版本的trx_id值為20,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
- 4.下一個(gè)版本的列name的內(nèi)容是“王五”,該版本的trx_id值也為10,也在trx_ids列表內(nèi),所以也不符合要求,同理下一個(gè)列name的內(nèi)容是“李四”的 版本也不符合要求,繼續(xù)跳到下一個(gè)版本。
- 5.下一個(gè)版本的name的內(nèi)容是“張三”,該版本的trx_id值為8,小于ReadView中的up_limit_id 值 10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列name為“張三”的記錄。
兩次select查詢得到的結(jié)果是重復(fù)的,記錄的列name值都是張三,這就是可重復(fù)讀的含義。如果我們之后再把事務(wù)id為20的記錄提交了,然后再到剛才使用 REPEATABLE READ 隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id為1的記錄,得到的記錄還是張三,具體過程可以自行分析。
【5】MVCC如何解決幻讀?
其實(shí)從前面我們流程分析也可以體會(huì)到,如果有新的事務(wù)進(jìn)行插入,那么其事務(wù)ID要么在trx_ids列表內(nèi),要么是大于等于low_limit_id 的,其對(duì)應(yīng)的行記錄版本顯示是要排除的。
假設(shè)現(xiàn)在表student中只有一條數(shù)據(jù),數(shù)據(jù)內(nèi)容中,主鍵id=1,隱藏的trx_id=10,它的undo log如下圖所示。
假設(shè)現(xiàn)在有事務(wù)A和事務(wù)B并發(fā)執(zhí)行,事務(wù)A的事務(wù)id為20,事務(wù)B的事務(wù)id為30。
步驟1:事務(wù)A開始第一次查詢數(shù)據(jù),查詢的SQL語句如下:
select * from student where id>1;在開始查詢之前,MySQL會(huì)為事務(wù)A產(chǎn)生一個(gè)ReadView,此時(shí)ReadView的內(nèi)容如下:trx_ids=[20,30],up_limit_id=20,low_limit_id=31,creator_trx_id=20。
由于此時(shí)表student中只有一條數(shù)據(jù),且符合whereid>=1 條件,因此會(huì)查詢出來。然后根據(jù)ReadView機(jī)制,發(fā)現(xiàn)該行數(shù)據(jù)的trx_id=10,小于事務(wù)A的ReadView里up_limit_id,這表示這條數(shù)據(jù)是事務(wù)A開啟之前,其他事務(wù)就已經(jīng)提交了的數(shù)據(jù),因此事務(wù)A可以讀取到。
結(jié)論:事務(wù)A的第一次查詢,能讀取到一條數(shù)據(jù),id。
步驟2:接著事務(wù)B(trx_id=30),往表student中新插入兩條數(shù)據(jù),并提交事務(wù)。
insert into student(id,name) values(2,'李四'); insert into student(id,name) values(3,'王五');此時(shí)表student中就有三條數(shù)據(jù)了,對(duì)應(yīng)的undo如下圖所示:
步驟3:接著事務(wù)A開啟第二次查詢,根據(jù)可重復(fù)讀隔離級(jí)別的規(guī)則,此時(shí)事務(wù)A并不會(huì)再重新生成ReadView。此時(shí)表student中的3條數(shù)據(jù)都滿足where id>=1的條件,因?yàn)闀?huì)先查出來。然后根據(jù)ReadView機(jī)制,判斷每條數(shù)據(jù)是不是都可以被事務(wù)A看到。
- 1.首先id =1的這條數(shù)據(jù),前面已經(jīng)說過了,可以被事務(wù)A看到
- 2.然后是id=2的數(shù)據(jù),它的trx_id=30,此時(shí)事務(wù)A發(fā)現(xiàn),這個(gè)值處于up_limit_id和low_limit_id之間,因此還需要再判斷30是否處于trx_ids數(shù)組內(nèi)。由于事務(wù)A的trx_ids=[20,30],因此在數(shù)組內(nèi),這表示id=2的這條數(shù)據(jù)是與事務(wù)A在同一時(shí)刻啟動(dòng)的其他事務(wù)提交的,所以這他哦數(shù)據(jù)不能讓事務(wù)A看到。
- 3.同理,id=3的這條數(shù)據(jù),trx_id也為30,因此也不能被事務(wù)A看見。
結(jié)論:最終事務(wù)A的第二次查詢,只能查詢出id=1的這條數(shù)據(jù)。這和事務(wù)A的第一次查詢的結(jié)果是一樣的,因此沒有出現(xiàn)幻讀現(xiàn)象,所以說在MySQL的可重復(fù)讀隔離級(jí)別下,不存在幻讀問題。
在MySQL的可重復(fù)讀隔離級(jí)別下一定不會(huì)發(fā)生幻讀現(xiàn)象嗎?可以參考博文:MySQL事務(wù)中幻讀實(shí)踐
【6】總結(jié)
本文學(xué)習(xí)了MVCC在READ COMMITTED 、REPEATABLE READ這兩種隔離級(jí)別的事務(wù)在執(zhí)行快照讀操作時(shí)訪問記錄的版本鏈的過程,這樣使不同事務(wù)的讀-寫,寫-讀操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。
核心點(diǎn)在于ReadView的原理,READ COMMITTED 、REPEATABLE READ這兩個(gè)隔離級(jí)別的一個(gè)很大不同就是生成ReadView的時(shí)機(jī)不同:
- READ COMMITTED在每一次進(jìn)行普通select操作前都會(huì)生成一個(gè)ReadView。
- REPEATABLE READ只在第一次進(jìn)行普通select操作前生成一個(gè)ReadView,之后的查詢操作都重復(fù)使用這個(gè)ReadView。
說明:我們之前說執(zhí)行delete語句或者更新主鍵的update語句并不會(huì)立即把對(duì)應(yīng)的記錄完全從頁面中刪除而是執(zhí)行一個(gè)所謂的delete mark操作,相當(dāng)于只是對(duì)記錄打上了一個(gè)刪除標(biāo)志位,這主要就是為MVCC服務(wù)的。
通過MVCC我們可以解決:
- 讀寫之間阻塞的問題。通過MVCC可以讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,這樣就可以提升事務(wù)并發(fā)處理能力。
- 降低了死鎖的概率。這是因?yàn)镸VCC采用了樂觀鎖的方式,讀取數(shù)據(jù)時(shí)并不需要加鎖,對(duì)于寫操作也只鎖定必要的行。
- 解決快照讀的問題。當(dāng)我們查詢數(shù)據(jù)庫在某個(gè)時(shí)間點(diǎn)的快照時(shí),只能看到這個(gè)時(shí)間點(diǎn)之前事務(wù)提交更新的結(jié)果,而不能看到這個(gè)時(shí)間點(diǎn)之后事務(wù)提交的更新結(jié)果。
總結(jié)
以上是生活随笔為你收集整理的认真学习MySQL中的MVCC机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php忘记登陆密码,ZBlogPHP忘记
- 下一篇: 通俗理解 mysql 的mvcc机制