【大廠面試03期】MySQL是怎麼解決幻讀問題的?
阿新 • • 發佈:2020-06-04
## 問題分析
### 首先幻讀是什麼?
根據MySQL文件上面的定義
> The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
幻讀指的是在一個事務內,同一SELECT語句在不同時間執行,得到不同的結果集時,就會發生所謂的幻讀問題。
可以看看下面的例子:
這是網上找的一張圖(事務的務字寫錯了,不過不影響我們理解)
![](https://images.xiaozhuanlan.com/photo/2020/1e1c5605388cd0ee396d035bab09f949.png)
假設這個例子中的MySQL的隔離級別是**提交讀**,也就是一個事務內可以讀到其他事務提交後的結果。
那麼事務1第一次查詢dept表中所有部門時,結果是沒有"研發部",但是由於隔離級別是**提交讀**,在事務2插入“研發部”這一行資料後,並且提交後,事務1是可以讀取到的,所以第二次查詢時,結果集中會有“研發部”。這就是幻讀。
### SELECT語句分類
首先我們的SELECT查詢分為快照讀和實時讀,快照讀通過MVCC(併發多版本控制)來解決幻讀問題,實時讀通過行鎖來解決幻讀問題。
### 快照讀
### 1.1 快照讀是什麼?
因為MySQL預設的隔離級別是**可重複讀**,這種隔離級別下,我們普通的SELECT語句都是快照讀,也就是在一個事務內,多次執行SELECT語句,查詢到的資料都是事務開始時那個狀態的資料(這樣就不會受其他事務修改資料的影響),這樣就解決了幻讀的問題。
### 1.2 那麼innodb是怎麼解決快照讀的幻讀問題的?
快照讀就是每一行資料中額外儲存兩個隱藏的列,插入這個資料行時的版本號,刪除這個資料行時的版本號(可能為空),滾動指標(指向undo log中用於事務回滾的日誌記錄)。
事務在對資料修改後,進行儲存時,如果資料行的當前版本號與事務開始取得資料的版本號一致就儲存成功,否則儲存失敗。
當我們不顯式使用BEGIN來開啟事務時,我們執行的每一條語句就是一個事務,每次開始事務時,會對系統版本號+1作為當前事務的ID。
#### 1.2.1插入操作
插入一行資料時,將事務的ID作為資料行的建立版本號。
#### 1.2.2刪除操作
執行刪除操作時,會將原資料行的刪除版本號設定為當前事務的ID,然後根據原資料行生成一條INSERT語句,寫入undo log,用於事務執行失敗時回滾。delete操作實際上不會直接刪除,而是將delete物件打上delete flag,標記為刪除,最終的刪除操作是purge執行緒完成的。但是會將資料行的刪除版本號設定為當前的事務的ID,這樣後面的事務B即便查到這行資料由於事務B的ID>刪除版本號,也會忽略這條資料。
#### 1.2.3更新操作
更新時可以簡單的認為是先將舊資料刪除,然後插入一條新資料。
所以執行更新操作時,其實是會將原資料行的刪除版本號設定為當前事務的ID,生成一條INSERT語句,寫入undo log,用於事務執行失敗時回滾。插入一條新的資料,將事務的ID作為資料行的的建立版本號。
#### 1.2.4查詢操作
資料行要被查詢出來必須滿足兩個條件,
- 資料行刪除版本號為空或者>當前事務版本號的資料(否則資料已經被標記刪除了)
- 建立版本號<=當前事務版本號的資料(否則資料是後面的事務創建出來的)
簡單來說,就是查詢時,
- 如果該行資料沒有被加行鎖中的X鎖(也就是沒有其他事務對這行資料進行修改),那麼直接讀取資料(前提是資料的版本號<=當前事務版本號的資料,不然不會放到查詢結果集裡面)。
- 該行資料被加了行鎖X鎖(也就是現在有其他事務對這行資料進行修改),那麼讀資料的事務不會進行等待,而是回去undo log端裡面讀之前版本的資料(這裡儲存的資料本身是用於回滾的),在**可重複讀**的隔離級別下,從undo log中讀取的資料總是事務開始時的快照資料(也就是版本號小於當前事務ID的資料),在**提交讀**的隔離級別下,從undo log中讀取的總是最新的快照資料。
### 1.3 補充資料:undo log段是什麼?
undo_log是一種邏輯日誌,是舊資料的備份。有兩個作用,用於事務回滾和為MVCC提供老版本的資料。
**可以認為當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的update記錄。**
#### 1.3.1.用於事務回滾
當事務執行失敗,回退時,會讀取這行資料的滾動指標(指向undo log中用於事務回滾的日誌記錄),就可以在undo log中找到相應的邏輯記錄,讀取到相應的回滾語句,執行進行回滾。
#### 1.3.2.為MVCC提供老版本的資料
當讀取的某一行被其他事務鎖定時(也就是有其他事務正在改這行資料),它可以從undo log中分析出該行記錄以前的資料是什麼,從而提供該行版本資訊,讓使用者進行快照讀。在**可重複讀**的隔離級別下,從undo log中讀取的資料總是事務開始時的快照資料(也就是版本號小於當前事務ID的資料),在**提交讀**的隔離級別下,從undo log中讀取的總是最新的快照資料(也就是比正在修改這行資料的事務ID修改前的資料。)。
### 實時讀
### 2.1實時讀是什麼?
如果說快照讀總是讀取事務開始時那個狀態的資料,實時讀就是查詢時總是執行這個查詢時資料庫中的資料。
一般使用以下這兩種查詢語句進行查詢時就是實時讀。
```
SELECT *** FOR UPDATE 在查詢時會先申請X鎖SELECT *** IN SHARE MODE 在查詢時會先申請S鎖
```
首先看一個實時讀產生幻讀的案例:
![](https://images.xiaozhuanlan.com/photo/2020/e444efe6c11c9d7def65050890d6bbd1.png)
![](https://images.xiaozhuanlan.com/photo/2020/21ce7d52c46c9082ef48439643e16417.png)
這是《MySQL技術內幕++InnoDB儲存引擎++第2版》裡面的一張圖,就是先將隔離級別設定為**提交讀**,這樣第一次執行 `SELECT...FOR UPDATE`查詢出來的資料是a:4,事務B插入了一條新的資料,再次執行 `SELECT...FOR UPDATE`語句時,查詢出來就是a:4,a:5兩條資料,這就是幻讀的問題。
### 2.1那麼innodb是怎麼解決實時讀的幻讀問題的?
如果我們不在一開始將將隔離級別設定為**提交讀**,其實是不會產生幻讀問題的,因為MySQL的預設隔離級別是**可重複讀**,在這種情況下,我們執行第一次 `SELECT...FOR UPDATE`查詢語句是,其實是會先申請行鎖,因為一開始資料庫就只有a:4一行資料,那麼加鎖區間其實是
```
(負無窮,4](4,正無窮)
```
我們查詢條件是a>2,上面兩個加鎖區間都會可能有資料滿足條件,所以會申請行鎖中的next-key lock,是會對上面這兩個區間都加鎖,這樣其他事務不能往這兩個區間插入資料,事務B會執行插入時會一直等待獲取鎖,直到事務A提交,釋放行鎖,事務B才有可能申請到鎖,然後進行插入。這樣就解決了幻讀問題。
如果大家對行鎖瞭解得比較少,下一期會對innodb中的鎖進行介紹。
## 最後
大家有什麼想法,可以一起討論!本文已收錄到1.1K Star數開源學習指南——《大廠面試指北》,如果想要了解更多大廠面試相關的內容,瞭解更多可以看
http://notfound9.github.io/interviewGuide/#/docs/BATInterview
![img](data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5Er