什么是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ù)釋放鎖。
快照讀與當(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ù)。 不加鎖的簡(jiǎn)單的 SELECT 都屬于快照讀 ,即不加鎖的非阻塞讀;比如這樣:
select * from table 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; # 共享鎖,S鎖
select * from student for update; # 排他鎖,X鎖
insert into student values ... # X鎖
delete from student where ... # X鎖
update student set ... # X鎖
前置知識(shí)
隔離級(jí)別
我們知道事務(wù)有4個(gè)隔離級(jí)別,存在3種并發(fā)問題。
在MySQL中,默認(rèn)的隔離級(jí)別是可重復(fù)讀,可以解決臟讀和不可重復(fù)讀的問題,如果僅從定義的角度來看,它并不能解決幻讀問題。如果我們想要解決幻讀問題,就需要采用串行化的方式,也就是將隔離級(jí)別提升到最高,但這樣一來就會(huì)大幅降低數(shù)據(jù)庫的事務(wù)并發(fā)能力。
而MVCC 可以不采用鎖機(jī)制,而是通過樂觀鎖的方式來解決不可重復(fù)讀和幻讀問題!它可以在大多數(shù)情況下替代行級(jí)鎖,降低系統(tǒng)的開銷。
隱藏字段,undo log版本鏈
回顧一下undo日志的版本鏈,對(duì)于使用InnoDB存儲(chǔ)引擎的表來說,它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列(字段)。
- 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ù)如下
mysql> select *from student;
+----+--------+--------+
| id | name | class |
+----+--------+--------+
| 1 | 張三 | 一班 |
+----+--------+--------+
1 row in set (0.01 sec)
假設(shè)插入該記錄的事務(wù)id為8,那么此刻該條記錄的示意圖如下所示:
insert undo只在事務(wù)回滾時(shí)起作用,當(dāng)事務(wù)提交后,該類型的undo日志就沒用了,它占用的UndoLog Segment也會(huì)被系統(tǒng)回收(也就是該undo日志占用的Undo頁面鏈表要么被重用,要么被釋放)。
假設(shè)之后兩個(gè)事務(wù)id分別為 10 、 20 的事務(wù)對(duì)這條記錄進(jìn)行UPDATE操作,操作流程如下:
能不能在兩個(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)前記錄最新的值。
每個(gè)版本中還包含生成該版本時(shí)對(duì)應(yīng)的事務(wù)id。
MVCC實(shí)現(xiàn)原理之ReadView
:::success
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ù)修改過的記錄。假如另一個(gè)事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是需要判斷一下版本鏈中的哪個(gè)版本是當(dāng)前事務(wù)可見的,這是ReadView要解決的主要問題。
ReadView中主要包含 4 個(gè)比較重要的內(nèi)容,分別如下:
-
creator_trx_id
,創(chuàng)建這個(gè) Read View 的事務(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為tr2、tr3、tr:5和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值,表明生成該版本的事務(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找到它:
- 首先獲取事務(wù)自己的版本號(hào),也就是事務(wù) ID;
- 生成 ReadView;
- 查詢得到的數(shù)據(jù),然后與 ReadView 中的事務(wù)版本號(hào)進(jìn)行比較;
- 如果不符合 ReadView 規(guī)則,就需要從 Undo Log 中獲取歷史快照;
- 最后返回符合規(guī)則的數(shù)據(jù)。
如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見的話,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見的話,那么就意味著該條記錄對(duì)該事務(wù)完全不可見,查詢結(jié)果就不包含該記錄。
lnnoDB中,MVCC是通過Undo Log + Read View進(jìn)行數(shù)據(jù)讀取,Undo Log保存了歷史快照,而Read View規(guī)則幫我們判斷當(dāng)前版本的數(shù)據(jù)是否可見。
在隔離級(jí)別為讀已提交(Read Committed)時(shí),一個(gè)事務(wù)中的每一次 SELECT 查詢都會(huì)重新獲取一次Read View。
如表所示:
注意,此時(shí)同樣的查詢語句都會(huì)重新獲取一次 Read View,這時(shí)如果 Read View 不同,就可能產(chǎn)生不可重復(fù)讀或者幻讀的情況。
當(dāng)隔離級(jí)別為可重復(fù)讀的時(shí)候,就避免了不可重復(fù)讀,這是因?yàn)橐粋€(gè)事務(wù)只在第一次 SELECT 的時(shí)候會(huì)獲取一次 Read View,而后面所有的 SELECT 都會(huì)復(fù)用這個(gè) Read View,如下表所示:
舉例說明整個(gè)流程
假設(shè)現(xiàn)在student表中只有一條由事務(wù)id為8的事務(wù)插入的一條記錄:
mysql> select *from student;
+----+--------+--------+
| id | name | class |
+----+--------+--------+
| 1 | 張三 | 一班 |
+----+--------+--------+
1 row in set (0.01 sec)
MVCC只能在READ COMMITTED
和REPEATABLE READ
兩個(gè)隔離級(jí)別下工作。接下來看一下READ COMMITTED
和REPEATABLE READ
所謂的生成ReadView的時(shí)機(jī)不同到底不同在哪里。
Read Commited隔離級(jí)別下
READ COMMITTED :每次讀取數(shù)據(jù)前都生成一個(gè)ReadView 。
現(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;
# 更新了一些別的表的記錄
...
說明:事務(wù)執(zhí)行過程中,只有在第一次真正修改記錄時(shí)(比如使用INSERT、DELETE、UPDATE語句),才會(huì)被 分配一個(gè)單獨(dú)的事務(wù)id,這個(gè)事務(wù)id是遞增的。所以我們才在事務(wù)20中更新些別的表的記錄,目的是讓它分配事務(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 的記錄的版本鏈就長(zhǎng)這樣:
然后再到剛才使用READ COMMITTED
隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id為 1 的記錄,如下:
# 使用READ COMMITTED隔離級(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的值為'王五'
這個(gè)SELECT2的執(zhí)行過程如下:
步驟1:在執(zhí)行SELECT語句時(shí)會(huì)又會(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為‘王五‘的記錄。
強(qiáng)調(diào): 使用READ COMMITTED隔離級(jí)別的事務(wù)在每次查詢開始時(shí)都會(huì)生成一個(gè)獨(dú)立的ReadView。
Repeatable Read隔離級(jí)別下
Repeatable Read:只會(huì)生成一個(gè)ReadView
比如,系統(tǒng)里有兩個(gè)事務(wù)id分別為 10 、 20 的事務(wù)在執(zhí)行:
# 開始記錄
mysql> select *from student;
+----+--------+--------+
| id | name | class |
+----+--------+--------+
| 1 | 張三 | 一班 |
+----+--------+--------+
1 row in set (0.01 sec)
# 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í)行:
# 使用REPEATABLE READ隔離級(jí)別的事務(wù)
BEGIN;
# SELECT1:Transaction 10、 20 未提交
SELECT * FROM student WHERE id = 1 ; # 得到的列name的值為'張三'
這個(gè)SELECT1的執(zhí)行過程如下(第一個(gè)ReadView和讀已提交是一樣的):
步驟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 的記錄的版本鏈長(zhǎng)這樣:
然后再到剛才使用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的值仍為'張三'
這個(gè)SELECT2的執(zhí)行過程如下:
步驟1:在執(zhí)行SELECT語句時(shí)會(huì)繼續(xù)使用之前的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列表中是包含值為10的事務(wù)id的,所以該版本也不符合要求,同理下一個(gè)列name的內(nèi)容是’李四’的版本也不符合要求。繼續(xù)跳到下一個(gè)版本。
步驟5∶下一個(gè)版本的列name的內(nèi)容是‘張三’,該版本的trx_id值為80,小于ReadView中的up_limit_id
值10,所以這個(gè)版本是符合要求的,最后返回給用戶的版本就是這條列c為‘張三’的記錄。
兩次SELECT查詢得到的結(jié)果是重復(fù)的,記錄的列c值都是‘張三’,這就是可重復(fù)讀的含義。如果我們之后再把事務(wù)id為20的記錄提交了,然后再到剛才使用REPEATABLE READ隔離級(jí)別的事務(wù)中繼續(xù)查找這個(gè)id為1的記錄,得到的結(jié)果還是‘張三’,具體執(zhí)行過程大家可以自己分析一下。
解決幻讀問題
假設(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ù),且符合 where id>=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=1。
步驟 2 :接著事務(wù) B(trx_id=30),往表 student 中新插入兩條數(shù)據(jù),并提交事務(wù)。文章來源:http://www.zghlxwxcb.cn/news/detail-721440.html
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 的條件,因此會(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í)別下,不存在幻讀問題。文章來源地址http://www.zghlxwxcb.cn/news/detail-721440.html
到了這里,關(guān)于多版本并發(fā)控制MVCC的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!