MySQL事務(二)事務隔離的實現原理:一致性讀
阿新 • • 發佈:2020-12-11
今天我們來學習一下MySQL的事務隔離是如何實現的。如果你對事務以及事務隔離級別還不太瞭解的話,這裡[左轉](https://www.cnblogs.com/liang24/p/14111897.html)。
好的,下面正式進入主題。事務隔離級別有4種:讀未提交、讀提交、可重複讀和序列化。首先我們來說一下讀未提交和序列化。
- 讀未提交:效能最好,因為不加鎖,所以可以理解為沒有隔離。
- 序列化:讀加共享鎖,其他事務可併發讀,但不能寫;寫加排他鎖,其他事務不能併發寫也不能併發讀。
這兩種方式要麼啥都不管,併發效能最好,但也最多問題;要麼管得很嚴,無法併發處理,實現簡單。
另外兩種,讀提交和可重複讀,的實現方式就有考究了。
## 可重複讀
首先我們來看一下可重複讀是如何實現的。
在可重複讀隔離級別下,事務在啟動的時候就“拍了個快照”,並且這個快照是基於整個庫的。而“快照”在計算機裡是拷貝了一份當前的副本檔案,但在資料庫併發訪問場景下,不可能真的拷貝一份資料副本。
實際上,這個快照是基於InnoDB在實現MVCC時用到的一致性讀檢視來實現的。
> MVCC的全稱是“多版本併發控制”。這項技術使得InnoDB的事務隔離級別下執行一致性讀操作有了保證,換言之,就是為了查詢一些正在被另一個事務更新的行,並且可以看到它們被更新之前的值。這是一個可以用來增強併發性的強大的技術,因為這樣的一來的話查詢就不用等待另一個事務釋放鎖。這項技術在資料庫領域並不是普遍使用的。一些其它的資料庫產品,以及MySQL其它的儲存引擎並不支援它。
### 如何實現“快照”
InnoDB裡面每個事務有一個唯一的事務ID,叫作transaction id。它是在事務開始的時候向InnoDB的事務系統申請的,是按申請順序嚴格遞增的。
資料表中的一行記錄,其實可能有多個版本(row),每個版本都有自己的row trx_id。如圖1所示:
圖1 行狀態變更圖 圖中虛線框裡是同一行資料的4個版本,當前最新版本是V4,k的值是22,它是被transaction id為25的事務更新的,因此它的row trx_id是25。 其實除了最新版本V4外,其他三個版本實際上是不存在的,它們是由undo log和最新版本資料計算得到的。其中undo段就是圖中的虛線箭頭的U1、U2、U3,例如V1版本就是根據V4依次執行U3、U2、U1計算得到的。 > undo log是回滾日誌,儲存的是邏輯格式的日誌,可用於事務回滾,也可以用於MVCC。 按照可重複讀的定義,一個事務啟動的時候,能夠看到所有已經提交的事務結果。但之後在這個事務執行期間,其他事務的更新對它不可見。 InnoDB為每個事務構成一個數組,用來儲存這個事務啟動瞬間,當前正在“活躍”的所有事務ID。“活躍”指啟動了但還沒有提交。 數組裡事務ID的最小值記為低水位,當前系統裡面已經建立過的事務ID的最大值加1記為高水位。這個檢視陣列加高水位就組成了當前事務的一致性檢視(read-view)。 而資料版本的可見性規則,就是基於資料的row trx_id和這個一致性檢視的對比結果得到的。
圖2 資料可見性規則 對於當前事務的啟動瞬間來說,一個數據版本的row trx_id有以下幾種可能: - 如果落在綠色部分,表示這個版本是已經提交的事務或者是當前事務自己生成的,這個資料是可見的。 - 如果落在紅色部分,表示這個版本是由將來啟動的事務生成的,不可見。 - 如果落在橙色部分,就有兩種情況: - 若row trx_id在陣列中,表示這個版本是由還沒提交的事務生成的,不可見。 - 若row trx_id不在陣列中,表示這個版本是已經提交了的事務生成的,可見。 比如圖1中的資料來說,如果有一個事務,它的低水位是18,那麼當它訪問這一行資料時,就會從V4通過U3計算出V3,所以在它看來,這一行的值是11。 ### 在更新時如何使用一致性讀
圖3 示例1 我們來看示例1,如果事務B在事務C更新之前查詢,這個查詢返回值是1。但是當它要去更新資料時,就不能在歷史版本上更新了,否則事務事務C的更新就會丟失。 這裡就用到一條規則:更新資料都是先讀後寫,而這個讀只能是讀當前的值,稱為“當前讀”(current read)。 因此事務B更新時,當前讀拿到的資料是(1, 2),更新後是(1, 3),並且row trx_id是101。 事務B後續查詢時,看到最新資料的版本號是101,而自己也是101,就直接返回,得到的k值是3。
圖4 示例2 我們再來看示例2,示例2與示例1的區別在於,事務C'在事務B的寫讀操作後提交。 事務C'在提交前對行加寫鎖。而事務B是當前讀,而且必須要加鎖,因此被鎖住了,必須等到事務C'釋放這個鎖,才能繼續它的當前讀。 到這裡,把一致性讀、當前讀和行鎖串起來了。 ### 小結 本節問題,事務的可重複讀隔離級別是如何實現的? 可重複讀的核心就是一致性讀;而事務更新資料的時候,只能用當前讀。如果當前的記得的行鎖被其他事務佔用的話,就需要進入鎖等待。 ## 讀提交 讀提交的實現方式跟可重複讀類似,它們最主要的區別是: - 在可重複讀隔離級別下,只需要在事務開始的時候建立一致性檢視,之後事務裡的其他查詢都共用這個一致性檢視; - 在讀提交隔離級別下,每個語句執行前都會重新算出一個新的檢視。 ## 參考資料 - [03 | 事務隔離:為什麼你改了我還看不見?](https://time.geekbang.org/column/article/68963) - [08 | 事務到底是隔離的還是不隔離的?](https://time.geekbang.org/column/article/70562) - [MySQL事務隔離級別的實現原理](https://www.cnblogs.com/cjsblog/p/83659
圖1 行狀態變更圖 圖中虛線框裡是同一行資料的4個版本,當前最新版本是V4,k的值是22,它是被transaction id為25的事務更新的,因此它的row trx_id是25。 其實除了最新版本V4外,其他三個版本實際上是不存在的,它們是由undo log和最新版本資料計算得到的。其中undo段就是圖中的虛線箭頭的U1、U2、U3,例如V1版本就是根據V4依次執行U3、U2、U1計算得到的。 > undo log是回滾日誌,儲存的是邏輯格式的日誌,可用於事務回滾,也可以用於MVCC。 按照可重複讀的定義,一個事務啟動的時候,能夠看到所有已經提交的事務結果。但之後在這個事務執行期間,其他事務的更新對它不可見。 InnoDB為每個事務構成一個數組,用來儲存這個事務啟動瞬間,當前正在“活躍”的所有事務ID。“活躍”指啟動了但還沒有提交。 數組裡事務ID的最小值記為低水位,當前系統裡面已經建立過的事務ID的最大值加1記為高水位。這個檢視陣列加高水位就組成了當前事務的一致性檢視(read-view)。 而資料版本的可見性規則,就是基於資料的row trx_id和這個一致性檢視的對比結果得到的。
圖2 資料可見性規則 對於當前事務的啟動瞬間來說,一個數據版本的row trx_id有以下幾種可能: - 如果落在綠色部分,表示這個版本是已經提交的事務或者是當前事務自己生成的,這個資料是可見的。 - 如果落在紅色部分,表示這個版本是由將來啟動的事務生成的,不可見。 - 如果落在橙色部分,就有兩種情況: - 若row trx_id在陣列中,表示這個版本是由還沒提交的事務生成的,不可見。 - 若row trx_id不在陣列中,表示這個版本是已經提交了的事務生成的,可見。 比如圖1中的資料來說,如果有一個事務,它的低水位是18,那麼當它訪問這一行資料時,就會從V4通過U3計算出V3,所以在它看來,這一行的值是11。 ### 在更新時如何使用一致性讀
圖3 示例1 我們來看示例1,如果事務B在事務C更新之前查詢,這個查詢返回值是1。但是當它要去更新資料時,就不能在歷史版本上更新了,否則事務事務C的更新就會丟失。 這裡就用到一條規則:更新資料都是先讀後寫,而這個讀只能是讀當前的值,稱為“當前讀”(current read)。 因此事務B更新時,當前讀拿到的資料是(1, 2),更新後是(1, 3),並且row trx_id是101。 事務B後續查詢時,看到最新資料的版本號是101,而自己也是101,就直接返回,得到的k值是3。
圖4 示例2 我們再來看示例2,示例2與示例1的區別在於,事務C'在事務B的寫讀操作後提交。 事務C'在提交前對行加寫鎖。而事務B是當前讀,而且必須要加鎖,因此被鎖住了,必須等到事務C'釋放這個鎖,才能繼續它的當前讀。 到這裡,把一致性讀、當前讀和行鎖串起來了。 ### 小結 本節問題,事務的可重複讀隔離級別是如何實現的? 可重複讀的核心就是一致性讀;而事務更新資料的時候,只能用當前讀。如果當前的記得的行鎖被其他事務佔用的話,就需要進入鎖等待。 ## 讀提交 讀提交的實現方式跟可重複讀類似,它們最主要的區別是: - 在可重複讀隔離級別下,只需要在事務開始的時候建立一致性檢視,之後事務裡的其他查詢都共用這個一致性檢視; - 在讀提交隔離級別下,每個語句執行前都會重新算出一個新的檢視。 ## 參考資料 - [03 | 事務隔離:為什麼你改了我還看不見?](https://time.geekbang.org/column/article/68963) - [08 | 事務到底是隔離的還是不隔離的?](https://time.geekbang.org/column/article/70562) - [MySQL事務隔離級別的實現原理](https://www.cnblogs.com/cjsblog/p/83659