?一致性非鎖定讀(consistent nonlocking read)是指InnoDB存儲引擎通過多版本控制(MVVC)讀取當(dāng)前數(shù)據(jù)庫中行數(shù)據(jù)的方式。如果讀取的行正在執(zhí)行DELETE或UPDATE操作,這時讀取操作不會因此去等待行上鎖的釋放。相反地,InnoDB會去讀取行的一個快照。

?上圖直觀地展現(xiàn)了InnoDB一致性非鎖定讀的機(jī)制。之所以稱其為非鎖定讀,是因?yàn)椴恍枰却猩吓潘i的釋放??煺諗?shù)據(jù)是指該行的之前版本的數(shù)據(jù),每行記錄可能有多個版本,一般稱這種技術(shù)為行多版本技術(shù)。由此帶來的并發(fā)控制,稱之為多版本并發(fā)控制(Multi Version Concurrency Control, MVVC)。InnoDB是通過undo log來實(shí)現(xiàn)MVVC。undo log本身用來在事務(wù)中回滾數(shù)據(jù),因此快照數(shù)據(jù)本身是沒有額外開銷。此外,讀取快照數(shù)據(jù)是不需要上鎖的,因?yàn)闆]有事務(wù)需要對歷史的數(shù)據(jù)進(jìn)行修改操作。
?一致性非鎖定讀是InnoDB默認(rèn)的讀取方式,即讀取不會占用和等待行上的鎖。但是并不是在每個事務(wù)隔離級別下都是采用此種方式。此外,即使都是使用一致性非鎖定讀,但是對于快照數(shù)據(jù)的定義也各不相同。
?在事務(wù)隔離級別READ COMMITTED和REPEATABLE READ下,InnoDB使用一致性非鎖定讀。然而,對于快照數(shù)據(jù)的定義卻不同。在READ COMMITTED事務(wù)隔離級別下,一致性非鎖定讀總是讀取被鎖定行的最新一份快照數(shù)據(jù)。而在REPEATABLE READ事務(wù)隔離級別下,則讀取事務(wù)開始時的行數(shù)據(jù)版本。
?我們下面舉個例子來詳細(xì)說明一下上述的情況。
# session A
mysql> BEGIN;
mysql> SELECT * FROM test WHERE id = 1;
?我們首先在會話A中顯示地開啟一個事務(wù),然后讀取test表中的id為1的數(shù)據(jù),但是事務(wù)并沒有結(jié)束。于此同時,用戶在開啟另一個會話B,這樣可以模擬并發(fā)的操作,然后對會話B做出如下的操作:
# session B
mysql> BEGIN;
mysql> UPDATE test SET id = 3 WHERE id = 1;
?在會話B的事務(wù)中,將test表中id為1的記錄修改為id=3,但是事務(wù)同樣也沒有提交,這樣id=1的行其實(shí)加了一個排他鎖。由于InnoDB在READ COMMITTED和REPEATABLE READ事務(wù)隔離級別下使用一致性非鎖定讀,這時如果會話A再次讀取id為1的記錄,仍然能夠讀取到相同的數(shù)據(jù)。此時,READ COMMITTED和REPEATABLE READ事務(wù)隔離級別沒有任何區(qū)別。

?如上圖所示,當(dāng)會話B提交事務(wù)后,會話A再次運(yùn)行SELECT * FROM test WHERE id = 1的SQL語句時,兩個事務(wù)隔離級別下得到的結(jié)果就不一樣了。
?對于READ COMMITTED的事務(wù)隔離級別,它總是讀取行的最新版本,如果行被鎖定了,則讀取該行版本的最新一個快照。因?yàn)闀払的事務(wù)已經(jīng)提交,所以在該隔離級別下上述SQL語句的結(jié)果集是空的。
?對于REPEATABLE READ的事務(wù)隔離級別,總是讀取事務(wù)開始時的行數(shù)據(jù),因此,在該隔離級別下,上述SQL語句仍然會獲得相同的數(shù)據(jù)。
MVVC
?我們首先來看一下wiki上對MVVC的定義:
Multiversion concurrency control (MCC or MVCC), is a concurrency control
method commonly used by database management systems to provide
concurrent access to the database and in programming languages to
implement transactional memory.
?由定義可知,MVVC是用于數(shù)據(jù)庫提供并發(fā)訪問控制的并發(fā)控制技術(shù)。
數(shù)據(jù)庫的并發(fā)控制機(jī)制有很多,最為常見的就是鎖機(jī)制。鎖機(jī)制一般會給競爭資源加鎖,阻塞讀或者寫操作來解決事務(wù)之間的競爭條件,最終保證事務(wù)的可串行化。而MVVC則引入了另外一種并發(fā)控制,它讓讀寫操作互不阻塞,每一個寫操作都會創(chuàng)建一個新版本的數(shù)據(jù),讀操作會從有限多個版本的數(shù)據(jù)中挑選一個最合適的結(jié)果直接返回,由此解決了事務(wù)的競爭條件。
?考慮一個現(xiàn)實(shí)場景。管理者要查詢所有用戶的存款總額,假設(shè)除了用戶A和用戶B之外,其他用戶的存款總額都為0,A、B用戶各有存款1000,所以所有用戶的存款總額為2000。但是在查詢過程中,用戶A會向用戶B進(jìn)行轉(zhuǎn)賬操作。轉(zhuǎn)賬操作和查詢總額操作的時序圖如下圖所示。

?如果沒有任何的并發(fā)控制機(jī)制,查詢總額事務(wù)先讀取了用戶A的賬戶存款,然后轉(zhuǎn)賬事務(wù)改變了用戶A和用戶B的賬戶存款,最后查詢總額事務(wù)繼續(xù)讀取了轉(zhuǎn)賬后的用戶B的賬號存款,導(dǎo)致最終統(tǒng)計的存款總額多了100元,發(fā)生錯誤。
?使用鎖機(jī)制可以解決上述的問題。查詢總額事務(wù)會對讀取的行加鎖,等到操作結(jié)束后再釋放所有行上的鎖。因?yàn)橛脩鬉的存款被鎖,導(dǎo)致轉(zhuǎn)賬操作被阻塞,直到查詢總額事務(wù)提交并將所有鎖都釋放。

?但是這時可能會引入新的問題,當(dāng)轉(zhuǎn)賬操作是從用戶B向用戶A進(jìn)行轉(zhuǎn)賬時會導(dǎo)致死鎖。轉(zhuǎn)賬事務(wù)會先鎖住用戶B的數(shù)據(jù),等待用戶A數(shù)據(jù)上的鎖,但是查詢總額的事務(wù)卻先鎖住了用戶A數(shù)據(jù),等待用戶B的數(shù)據(jù)上的鎖。
?使用MVVC機(jī)制也可以解決這個問題。查詢總額事務(wù)先讀取了用戶A的賬戶存款,然后轉(zhuǎn)賬事務(wù)會修改用戶A和用戶B賬戶存款,查詢總額事務(wù)讀取用戶B存款時不會讀取轉(zhuǎn)賬事務(wù)修改后的數(shù)據(jù),而是讀取本事務(wù)開始時的數(shù)據(jù)副本(在REPEATABLE READ隔離等級下)。

?MVCC使得數(shù)據(jù)庫讀不會對數(shù)據(jù)加鎖,普通的SELECT請求不會加鎖,提高了數(shù)據(jù)庫的并發(fā)處理能力。借助MVCC,數(shù)據(jù)庫可以實(shí)現(xiàn)READ COMMITTED,REPEATABLE READ等隔離級別,用戶可以查看當(dāng)前數(shù)據(jù)的前一個或者前幾個歷史版本,保證了ACID中的I特性(隔離性)
InnoDB的MVVC實(shí)現(xiàn)
?多版本并發(fā)控制僅僅是一種技術(shù)概念,并沒有統(tǒng)一的實(shí)現(xiàn)標(biāo)準(zhǔn), 其的核心理念就是數(shù)據(jù)快照,不同的事務(wù)訪問不同版本的數(shù)據(jù)快照,從而實(shí)現(xiàn)不同的事務(wù)隔離級別。雖然字面上是說具有多個版本的數(shù)據(jù)快照,但這并不意味著數(shù)據(jù)庫必須拷貝數(shù)據(jù),保存多份數(shù)據(jù)文件,這樣會浪費(fèi)大量的存儲空間。InnoDB通過事務(wù)的undo日志巧妙地實(shí)現(xiàn)了多版本的數(shù)據(jù)快照。
?數(shù)據(jù)庫的事務(wù)有時需要進(jìn)行回滾操作,這時就需要對之前的操作進(jìn)行undo。因此,在對數(shù)據(jù)進(jìn)行修改時,InnoDB會產(chǎn)生undo log。當(dāng)事務(wù)需要進(jìn)行回滾時,InnoDB可以利用這些undo log將數(shù)據(jù)回滾到修改之前的樣子。
?根據(jù)行為的不同 undo log 分為兩種 insert undo log和update undo log。
?insert undo log 是在 insert 操作中產(chǎn)生的 undo log。因?yàn)?insert 操作的記錄只對事務(wù)本身可見,對于其它事務(wù)此記錄是不可見的,所以 insert undo log 可以在事務(wù)提交后直接刪除而不需要進(jìn)行 purge 操作。
?update undo log 是 update 或 delete 操作中產(chǎn)生的 undo log,因?yàn)闀σ呀?jīng)存在的記錄產(chǎn)生影響,為了提供 MVCC機(jī)制,因此 update undo log 不能在事務(wù)提交時就進(jìn)行刪除,而是將事務(wù)提交時放到入 history list 上,等待 purge 線程進(jìn)行最后的刪除操作。
?為了保證事務(wù)并發(fā)操作時,在寫各自的undo log時不產(chǎn)生沖突,InnoDB采用回滾段的方式來維護(hù)undo log的并發(fā)寫入和持久化。回滾段實(shí)際上是一種 Undo 文件組織方式。
?InnoDB行記錄有三個隱藏字段:分別對應(yīng)該行的rowid、事務(wù)號db_trx_id和回滾指針db_roll_ptr,其中db_trx_id表示最近修改的事務(wù)的id,db_roll_ptr指向回滾段中的undo log。如下圖所示。

?當(dāng)事務(wù)2使用UPDATE語句修改該行數(shù)據(jù)時,會首先使用排他鎖鎖定改行,將該行當(dāng)前的值復(fù)制到undo log中,然后再真正地修改當(dāng)前行的值,最后填寫事務(wù)ID,使用回滾指針指向undo log中修改前的行。如下圖所示。

?當(dāng)事務(wù)3進(jìn)行修改與事務(wù)2的處理過程類似,如下圖所示。

?REPEATABLE READ隔離級別下事務(wù)開始后使用MVVC機(jī)制進(jìn)行讀取時,會將當(dāng)時活動的事務(wù)id記錄下來,記錄到Read View中。READ COMMITTED隔離級別下則是每次讀取時都創(chuàng)建一個新的Read View。
?Read View是InnoDB中用于判斷記錄可見性的數(shù)據(jù)結(jié)構(gòu),記錄了一些用于判斷可見性的屬性。
- low_limit_id:某行記錄的db_trx_id < 該值,則該行對于當(dāng)前Read View是一定可見的
- up_limit_id:某行記錄的db_trx_id >= 該值,則該行對于當(dāng)前read view是一定不可見的
- low_limit_no:用于purge操作的判斷
- rw_trx_ids:讀寫事務(wù)數(shù)組
?Read View創(chuàng)建后,事務(wù)再次進(jìn)行讀操作時比較記錄的db_trx_id和Read View中的low_limit_id,up_limit_id和讀寫事務(wù)數(shù)組來判斷可見性。
?如果該行中的db_trx_id等于當(dāng)前事務(wù)id,說明是事務(wù)內(nèi)部發(fā)生的更改,直接返回該行數(shù)據(jù)。否則的話,如果db_trx_id小于up_limit_id,說明是事務(wù)開始前的修改,則該記錄對當(dāng)前Read View是可見的,直接返回該行數(shù)據(jù)。
?如果db_trx_id大于或者等于low_limit_id,則該記錄對于該Read View一定是不可見的。如果db_trx_id位于[up_limit_id, low_limit_id)范圍內(nèi),需要在活躍讀寫事務(wù)數(shù)組(rw_trx_ids)中查找db_trx_id是否存在,如果存在,記錄對于當(dāng)前Read View是不可見的。
?如果記錄對于Read View不可見,需要通過記錄的DB_ROLL_PTR指針遍歷undo log,構(gòu)造對當(dāng)前Read View可見版本數(shù)據(jù)。
?簡單來說,Read View記錄讀開始時及其之后,所有的活動事務(wù),這些事務(wù)所做的修改對于Read View是不可見的。除此之外,所有其他的小于創(chuàng)建Read View的事務(wù)號的所有記錄均可見。
后記
?我們后續(xù)還會學(xué)習(xí)InnoDB的鎖的相關(guān)的知識,請大家持續(xù)關(guān)注。
- Mysql探索(一):B-Tree索引
- 數(shù)據(jù)庫內(nèi)部存儲結(jié)構(gòu)探索
- MySQL探秘(二):SQL語句執(zhí)行過程詳解
- MySQL探秘(三):InnoDB的內(nèi)存結(jié)構(gòu)和特性
- MySQL探秘(四):InnoDB的磁盤文件及落盤機(jī)制
- MySQL探秘(五):InnoDB鎖的類型和狀態(tài)查詢

參考文章
- http://mysql.taobao.org/monthly/2018/03/01/
- https://liuzhengyang.github.io/2017/04/18/innodb-mvcc/
- http://hedengcheng.com/?p=148
- 《唐成-2016PG大會-數(shù)據(jù)庫多版本實(shí)現(xiàn)內(nèi)幕.pdf》