MySQL探祕(六):InnoDB一致性非鎖定讀

 一致性非鎖定讀(consistent nonlocking read)是指InnoDB存儲引擎經過多版本控制(MVVC)讀取當前數據庫中行數據的方式。若是讀取的行正在執行DELETE或UPDATE操做,這時讀取操做不會所以去等待行上鎖的釋放。相反地,InnoDB會去讀取行的一個快照。mysql

一致性非鎖定讀示意圖

 上圖直觀地展示了InnoDB一致性非鎖定讀的機制。之因此稱其爲非鎖定讀,是由於不須要等待行上排他鎖的釋放。快照數據是指該行的以前版本的數據,每行記錄可能有多個版本,通常稱這種技術爲行多版本技術。由此帶來的併發控制,稱之爲多版本併發控制(Multi Version Concurrency Control, MVVC)。InnoDB是經過undo log來實現MVVC。undo log自己用來在事務中回滾數據,所以快照數據自己是沒有額外開銷。此外,讀取快照數據是不須要上鎖的,由於沒有事務須要對歷史的數據進行修改操做。git

 一致性非鎖定讀是InnoDB默認的讀取方式,即讀取不會佔用和等待行上的鎖。可是並非在每一個事務隔離級別下都是採用此種方式。此外,即便都是使用一致性非鎖定讀,可是對於快照數據的定義也各不相同。github

 在事務隔離級別READ COMMITTED和REPEATABLE READ下,InnoDB使用一致性非鎖定讀。然而,對於快照數據的定義卻不一樣。在READ COMMITTED事務隔離級別下,一致性非鎖定讀老是讀取被鎖定行的最新一份快照數據。而在REPEATABLE READ事務隔離級別下,則讀取事務開始時的行數據版本。sql

 咱們下面舉個例子來詳細說明一下上述的狀況。數據庫

# session A
mysql> BEGIN;
mysql> SELECT * FROM test WHERE id = 1;

 咱們首先在會話A中顯示地開啓一個事務,而後讀取test表中的id爲1的數據,可是事務並無結束。於此同時,用戶在開啓另外一個會話B,這樣能夠模擬併發的操做,而後對會話B作出以下的操做:數組

# session B
mysql> BEGIN;
mysql> UPDATE test SET id = 3 WHERE id = 1;

 在會話B的事務中,將test表中id爲1的記錄修改成id=3,可是事務一樣也沒有提交,這樣id=1的行其實加了一個排他鎖。因爲InnoDB在READ COMMITTED和REPEATABLE READ事務隔離級別下使用一致性非鎖定讀,這時若是會話A再次讀取id爲1的記錄,仍然可以讀取到相同的數據。此時,READ COMMITTED和REPEATABLE READ事務隔離級別沒有任何區別。session

會話A和會話B示意圖

 如上圖所示,當會話B提交事務後,會話A再次運行SELECT * FROM test WHERE id = 1的SQL語句時,兩個事務隔離級別下獲得的結果就不同了。
 對於READ COMMITTED的事務隔離級別,它老是讀取行的最新版本,若是行被鎖定了,則讀取該行版本的最新一個快照。由於會話B的事務已經提交,因此在該隔離級別下上述SQL語句的結果集是空的。
 對於REPEATABLE READ的事務隔離級別,老是讀取事務開始時的行數據,所以,在該隔離級別下,上述SQL語句仍然會得到相同的數據。數據結構

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是用於數據庫提供併發訪問控制的併發控制技術。
數據庫的併發控制機制有不少,最爲常見的就是鎖機制。鎖機制通常會給競爭資源加鎖,阻塞讀或者寫操做來解決事務之間的競爭條件,最終保證事務的可串行化。而MVVC則引入了另一種併發控制,它讓讀寫操做互不阻塞,每個寫操做都會建立一個新版本的數據,讀操做會從有限多個版本的數據中挑選一個最合適的結果直接返回,由此解決了事務的競爭條件。
 考慮一個現實場景。管理者要查詢全部用戶的存款總額,假設除了用戶A和用戶B以外,其餘用戶的存款總額都爲0,A、B用戶各有存款1000,因此全部用戶的存款總額爲2000。可是在查詢過程當中,用戶A會向用戶B進行轉帳操做。轉帳操做和查詢總額操做的時序圖以下圖所示。mvc

轉帳和查詢的時序圖

 若是沒有任何的併發控制機制,查詢總額事務先讀取了用戶A的帳戶存款,而後轉帳事務改變了用戶A和用戶B的帳戶存款,最後查詢總額事務繼續讀取了轉帳後的用戶B的帳號存款,致使最終統計的存款總額多了100元,發生錯誤。

 使用鎖機制能夠解決上述的問題。查詢總額事務會對讀取的行加鎖,等到操做結束後再釋放全部行上的鎖。由於用戶A的存款被鎖,致使轉帳操做被阻塞,直到查詢總額事務提交併將全部鎖都釋放。

使用鎖機制

 可是這時可能會引入新的問題,當轉帳操做是從用戶B向用戶A進行轉帳時會致使死鎖。轉帳事務會先鎖住用戶B的數據,等待用戶A數據上的鎖,可是查詢總額的事務卻先鎖住了用戶A數據,等待用戶B的數據上的鎖。

 使用MVVC機制也能夠解決這個問題。查詢總額事務先讀取了用戶A的帳戶存款,而後轉帳事務會修改用戶A和用戶B帳戶存款,查詢總額事務讀取用戶B存款時不會讀取轉帳事務修改後的數據,而是讀取本事務開始時的數據副本(在REPEATABLE READ隔離等級下)。

使用MVVC機制

 MVCC使得數據庫讀不會對數據加鎖,普通的SELECT請求不會加鎖,提升了數據庫的併發處理能力。藉助MVCC,數據庫能夠實現READ COMMITTED,REPEATABLE READ等隔離級別,用戶能夠查看當前數據的前一個或者前幾個歷史版本,保證了ACID中的I特性(隔離性)

InnoDB的MVVC實現

 多版本併發控制僅僅是一種技術概念,並無統一的實現標準, 其的核心理念就是數據快照,不一樣的事務訪問不一樣版本的數據快照,從而實現不一樣的事務隔離級別。雖然字面上是說具備多個版本的數據快照,但這並不意味着數據庫必須拷貝數據,保存多份數據文件,這樣會浪費大量的存儲空間。InnoDB經過事務的undo日誌巧妙地實現了多版本的數據快照。

 數據庫的事務有時須要進行回滾操做,這時就須要對以前的操做進行undo。所以,在對數據進行修改時,InnoDB會產生undo log。當事務須要進行回滾時,InnoDB能夠利用這些undo log將數據回滾到修改以前的樣子。

 根據行爲的不一樣 undo log 分爲兩種 insert undo log和update undo log。
 insert undo log 是在 insert 操做中產生的 undo log。由於 insert 操做的記錄只對事務自己可見,對於其它事務此記錄是不可見的,因此 insert undo log 能夠在事務提交後直接刪除而不須要進行 purge 操做。

 update undo log 是 update 或 delete 操做中產生的 undo log,由於會對已經存在的記錄產生影響,爲了提供 MVCC機制,所以 update undo log 不能在事務提交時就進行刪除,而是將事務提交時放到入 history list 上,等待 purge 線程進行最後的刪除操做。

 爲了保證事務併發操做時,在寫各自的undo log時不產生衝突,InnoDB採用回滾段的方式來維護undo log的併發寫入和持久化。回滾段其實是一種 Undo 文件組織方式。

 InnoDB行記錄有三個隱藏字段:分別對應該行的rowid、事務號db_trx_id和回滾指針db_roll_ptr,其中db_trx_id表示最近修改的事務的id,db_roll_ptr指向回滾段中的undo log。以下圖所示。

初始狀態

 當事務2使用UPDATE語句修改該行數據時,會首先使用排他鎖鎖定改行,將該行當前的值複製到undo log中,而後再真正地修改當前行的值,最後填寫事務ID,使用回滾指針指向undo log中修改前的行。以下圖所示。

第一次修改

 當事務3進行修改與事務2的處理過程相似,以下圖所示。

第二次修改

 REPEATABLE READ隔離級別下事務開始後使用MVVC機制進行讀取時,會將當時活動的事務id記錄下來,記錄到Read View中。READ COMMITTED隔離級別下則是每次讀取時都建立一個新的Read View。
 Read View是InnoDB中用於判斷記錄可見性的數據結構,記錄了一些用於判斷可見性的屬性。

  • low_limit_id:某行記錄的db_trx_id < 該值,則該行對於當前Read View是必定可見的
  • up_limit_id:某行記錄的db_trx_id >= 該值,則該行對於當前read view是必定不可見的
  • low_limit_no:用於purge操做的判斷
  • rw_trx_ids:讀寫事務數組

 Read View建立後,事務再次進行讀操做時比較記錄的db_trx_id和Read View中的low_limit_id,up_limit_id和讀寫事務數組來判斷可見性。

 若是該行中的db_trx_id等於當前事務id,說明是事務內部發生的更改,直接返回該行數據。不然的話,若是db_trx_id小於up_limit_id,說明是事務開始前的修改,則該記錄對當前Read View是可見的,直接返回該行數據。

 若是db_trx_id大於或者等於low_limit_id,則該記錄對於該Read View必定是不可見的。若是db_trx_id位於[up_limit_id, low_limit_id)範圍內,須要在活躍讀寫事務數組(rw_trx_ids)中查找db_trx_id是否存在,若是存在,記錄對於當前Read View是不可見的。
 若是記錄對於Read View不可見,須要經過記錄的DB_ROLL_PTR指針遍歷undo log,構造對當前Read View可見版本數據。
 簡單來講,Read View記錄讀開始時及其以後,全部的活動事務,這些事務所作的修改對於Read View是不可見的。除此以外,全部其餘的小於建立Read View的事務號的全部記錄都可見。

後記

 咱們後續還會學習InnoDB的鎖的相關的知識,請你們持續關注。

參考文章

相關文章
相關標籤/搜索