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

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

一致性非鎖定讀示意圖

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

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

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

 我們下面舉個例子來詳細說明一下上述的情況。

# 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事務隔離級別沒有任何區別。

會話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進行轉賬操作。轉賬操作和查詢總額操作的時序圖如下圖所示。

轉賬和查詢的時序圖

 如果沒有任何的併發控制機制,查詢總額事務先讀取了用戶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的鎖的相關的知識,請大家持續關注。

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章