關(guān)于MVCC,我之前寫錯(cuò)了,這次我改好了!
簡(jiǎn)單理解版
以下先引用我之前寫過的那篇中的內(nèi)容,可以快速理解,建議先簡(jiǎn)單看看。
要說幻讀,首先要了解MVCC,MVCC叫做多版本并發(fā)控制,實(shí)際上就是保存了數(shù)據(jù)在某個(gè)時(shí)間節(jié)點(diǎn)的快照。
我們每行數(shù)據(jù)實(shí)際上隱藏了兩列,創(chuàng)建時(shí)間版本號(hào),過期(刪除)時(shí)間版本號(hào),每開始一個(gè)新的事務(wù),版本號(hào)都會(huì)自動(dòng)遞增。
還是拿上面的user表舉例子,假設(shè)我們插入兩條數(shù)據(jù),他們實(shí)際上應(yīng)該長(zhǎng)這樣。
這時(shí)候假設(shè)小明去執(zhí)行查詢,此時(shí)current_version=3
- select * from user where id<=3;
同時(shí),小紅在這時(shí)候開啟事務(wù)去修改id=1的記錄,current_version=4
- update user set name='張三三' where id=1;
執(zhí)行成功后的結(jié)果是這樣的
如果這時(shí)候還有小黑在刪除id=2的數(shù)據(jù),current_version=5,執(zhí)行后結(jié)果是這樣的。
由于MVCC的原理是查找創(chuàng)建版本小于或等于當(dāng)前事務(wù)版本,刪除版本為空或者大于當(dāng)前事務(wù)版本,小明的真實(shí)的查詢應(yīng)該是這樣
- select * from user where id<=3 and create_version<=3 and (delete_version>3 or delete_version is null);
所以小明最后查詢到的id=1的名字還是'張三',并且id=2的記錄也能查詢到。這樣做是為了保證事務(wù)讀取的數(shù)據(jù)是在事務(wù)開始前就已經(jīng)存在的,要么是事務(wù)自己插入或者修改的。
真正原理
事實(shí)上,上述的說法只是簡(jiǎn)化版的理解,真正的MVCC用于讀已提交和可重復(fù)讀級(jí)別的控制,主要通過undo log日志版本鏈和read view來實(shí)現(xiàn)。
每條數(shù)據(jù)隱藏的兩個(gè)字段也并不是創(chuàng)建時(shí)間版本號(hào)和過期(刪除)時(shí)間版本號(hào),而是roll_pointer和trx_id。
roll_pointer指向更新事務(wù)之前生成的undo log,undo log用于事務(wù)的回滾,保證事務(wù)的原子性。
trx_id就是最近一次更新數(shù)據(jù)的事務(wù)ID。
以上述例子來舉例,最初插入兩條數(shù)據(jù),真實(shí)的情況是這樣,因?yàn)榈谝淮尾迦霐?shù)據(jù)沒有undo log,所以roll_pointer指向一個(gè)空的undo log。
這時(shí)候假設(shè)小明去執(zhí)行查詢,就會(huì)開啟一個(gè)read view,read view包含幾個(gè)重要的東西。
- m_ids,就是還未提交的事務(wù)id集合
- low_limit_id,m_ids里最小的值
- up_limit_id,m_ids里的最大值
- creator_trx_id,創(chuàng)建read view的事務(wù)ID,也就是自己的事務(wù)ID
小明來執(zhí)行查詢了,當(dāng)前事務(wù)ID=3
- select * from user where id<=3;
小紅在這時(shí)候開啟事務(wù)去修改id=1的記錄,事務(wù)ID=4
- update user set name='張三三' where id=1;
這時(shí)候小明的read view是這樣。
- m_ids=[3,4]
- low_limit_id=3
- up_limit_id=5
- creator_trx_id=3
所以,小明在執(zhí)行查詢的時(shí)候,會(huì)去判斷當(dāng)前這條數(shù)據(jù)的trx_id
這時(shí)候,小紅的修改也完成了,小紅數(shù)據(jù)于是就變成了這樣。
如果小明再次去查詢的話,就會(huì)發(fā)現(xiàn)現(xiàn)在的trx_id>read view的low_limit_id,也就是4>3,不符合條件,同時(shí)發(fā)現(xiàn)現(xiàn)在的trx_id=4在low_limit_id和up_limit_id [3,5]之間,并且trx_id=4在m_ids=[3,4]之中,所以就會(huì)根據(jù)roll_pointer指向的undo log去查找,trx_id=1小于現(xiàn)在的low_limit_id=3,符合條件,就找到了上一個(gè)版本name=張三的記錄。
如果這時(shí)候小明自己去修改這條記錄的值,把名字改成張五,結(jié)果就是這樣。
然后小明去查詢的話,就會(huì)發(fā)現(xiàn)當(dāng)前的trx_id=3就是自己的creator_trx_id,就是自己,那么就直接返回這條數(shù)據(jù)。
所以,我們可以先總結(jié)下幾種情況:
- 如果trx_id次開啟事務(wù)查詢的場(chǎng)景<>
- 如果trx_id>low_limit,trx_id還在[low_limit_id,up_limit_id]范圍之內(nèi),并且trx_id在m_ids中,就會(huì)根據(jù)roll_pointer去查找undo log日志鏈,找到之前版本的數(shù)據(jù),對(duì)應(yīng)的就是小紅修改后小明再次查詢的場(chǎng)景
- 如果trx_id=creator_trx_id,那么說明就是自己修改的,直接返回就好了,對(duì)應(yīng)的就是小明自己去修改數(shù)據(jù)的場(chǎng)景
不同隔離級(jí)別的實(shí)現(xiàn)
根據(jù)上面闡述的原理,你可能發(fā)現(xiàn)了,這是可重復(fù)讀下的實(shí)現(xiàn)啊,保證每次讀取到的數(shù)據(jù)都是一致的。
那么,如果是讀已提交級(jí)別下,這個(gè)是怎么實(shí)現(xiàn)的?
其實(shí)很簡(jiǎn)單,在上面的原理解釋中,我都是假設(shè)每次查詢的時(shí)候生成了read view,后續(xù)并沒有重新生成。
而讀已提交級(jí)別下,則是每次查詢都會(huì)生成一次read view。
以上述小紅修改過張三后的場(chǎng)景來舉例。
在可重復(fù)度級(jí)別下,由于trx_id>low_limit,trx_id還在[low_limit_id,up_limit_id]范圍之內(nèi),并且trx_id在m_ids中,滿足我們上述的條件2,所以就會(huì)根據(jù)roll_pointer找到之前的版本記錄,保證可重復(fù)讀。
而在讀已提交的級(jí)別下,重新生成了read view,這時(shí)候trx_id不在m_ids之中,說明事務(wù)已經(jīng)提交,所以可以直接返回這條數(shù)據(jù),所以查到的數(shù)據(jù)就是小紅修改后的name=張三三的數(shù)據(jù)了。
本文轉(zhuǎn)載自微信公眾號(hào)「艾小仙」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系艾小仙公眾號(hào)。