天天看點

【大廠面試03期】MySQL是怎麼解決幻讀問題的?

問題分析

首先幻讀是什麼?

根據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語句在不同時間執行,得到不同的結果集時,就會發生所謂的幻讀問題。

可以看看下面的例子:

這是網上找的一張圖(事務的務字寫錯了,不過不影響我們了解)

【大廠面試03期】MySQL是怎麼解決幻讀問題的?

假設這個例子中的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鎖
           

首先看一個實時讀産生幻讀的案例:

【大廠面試03期】MySQL是怎麼解決幻讀問題的?
【大廠面試03期】MySQL是怎麼解決幻讀問題的?

這是《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