MySQL原理簡介—8.MySQL併發事務處理

东阳马生架构發表於2024-11-25

大綱

1.簡單總結增刪改SQL語句的實現原理

2.多個事務同時執行的場景遇到的問題

3.多個事務併發更新或查詢時可能出現的問題

4.SQL標準中對事務的4個隔離級別

5.MySQL是如何支援4種事務隔離級別的

6.Spring事務註解瞭如何設定隔離級別

7.uodo log多版本鏈介紹

8.基於undo log多版本鏈實現ReadView機制

9.RC隔離級別如何基於ReadView機制實現

10.RR隔離級別如何基於ReadView機制實現

11.多事務併發執行的隔離機制總結

12.多事務更新同一行資料如何加鎖避免髒寫

13.共享鎖和獨佔鎖是什麼

14.什麼操作會導致表級別加鎖

15.表鎖和行鎖相互之間的關係以及互斥規則

16.資料庫出現不確定的效能抖動的原因

17.如何最佳化資料庫不確定性的效能抖動

1.簡單總結增刪改SQL語句的實現原理

其中會涉及MySQL的Buffer Pool機制、redo log機制和undo log機制。MySQL執行增刪改時,首先從磁碟載入資料頁到Buffer Pool的快取頁。然後更新快取頁,同時會記錄undo log回滾日誌和redo log重做日誌。這兩種日誌分別用於事務提交後當機恢復資料的場景及事務回滾的場景。

2.多個事務同時執行的場景遇到的問題

(1)一個事務會包含多條SQL語句

(2)業務系統多執行緒併發對MySQL執行事務

(3)併發執行多個MySQL事務可能遇到的問題

(1)一個事務會包含多條SQL語句

通常會在業務系統裡開啟事務來執行增刪改操作。

@Transactional
public void doService() {
    //增加一條資料
    addUser();
    //修改一條資料
    updateUser();
    //刪除一條資料
    deleteUser();
}

一般來說,業務系統會執行一個一個的事務。每個事務裡可能是一條或者多條增刪改的SQL語句。而事務的概念,就是一個事務裡的SQL語句要不一起成功提交,要不只要一條SQL失敗則事務就需回滾,撤銷所有SQL所做的修改。

(2)業務系統多執行緒併發對MySQL執行事務

此外,業務系統不會是單執行緒系統,它一般會有很多執行緒。於是一個業務系統可能基於多執行緒併發對MySQL資料庫執行多個事務。

MySQL原理簡介—8.MySQL併發事務處理

那麼每個事務裡面的多個SQL語句都是如何執行的呢?其實和普通增刪改SQL語句的執行原理一樣,都是先從磁碟載入資料頁到Buffer Pool的快取頁裡,然後更新Buffer Pool裡的快取頁,同時記錄redo log和undo log。如下圖所示:

MySQL原理簡介—8.MySQL併發事務處理

(3)併發執行多個MySQL事務可能遇到的問題

問題一:

多個事務併發執行時,可能會同時對快取頁裡的同一行資料進行更新。這裡併發更新同一行資料的衝突如何處理,是否透過加鎖進行處理。

問題二:

多個事務併發執行時,可能有的事務在進行更新,有的事務在進行查詢。這裡併發更新和查詢同一行資料的衝突應該如何處理。

要解決這些問題,就涉及同時寫和同時讀寫的併發衝突處理機制。其中就包括了MySQL事務的隔離級別、MVCC多版本隔離、鎖機制等。

3.多個事務併發更新或查詢時可能出現的問題

(1)髒寫

(2)髒讀

(3)不可重複讀

(4)幻讀

(1)髒寫

髒寫指的是,事務A和事務B同時在更新一條資料,事務A先把它更新為A值,事務B接著把它更新為B值:

MySQL原理簡介—8.MySQL併發事務處理

可見,事務B是後更新那一行資料的,所以此時那一行資料的值是B值。事務A更新後會記錄一條undo log,假設事務A更新前這行資料是NULL。那麼事務A的undo log大概是:更新前這行資料的值為NULL,主鍵為xx。

事務B更新資料的值為B之後,事務A突然需要使用它的undo log來回滾。於是事務A只要一回滾就會直接把那一行資料的值更新回之前的NULL值:

MySQL原理簡介—8.MySQL併發事務處理

這時對於事務B來說,值明明已經更新了,卻被事務A回滾,這就是髒寫。所謂的髒寫就是明明寫了一個資料值,結果過了一會兒這個資料值沒了。髒寫的定義就是事務B修改還沒提交的事務A修改過的資料。因為事務A隨時會回滾,所以會導致事務B修改的值也沒了。

(2)髒讀

假設事務A更新了一行資料的值由原來的NULL值變為A值,此時事務B去查詢了一下這行資料的值,看到的就是A值。

MySQL原理簡介—8.MySQL併發事務處理

於是業務系統的事務B拿到剛查出來的A值去進行一些業務處理,但是事務A突然回滾了事務,導致剛才更新的A值回滾成為NULL值。然後事務B此時再次查詢那一行資料的值,看到的只能是NULL值。

MySQL原理簡介—8.MySQL併發事務處理

髒讀的定義就是事務B查詢還沒提交的事務A修改過的資料。因為事務A隨時會回滾,可能導致事務B再次查詢就讀不到之前的資料。也就是導致事務B在前後時間點查詢同一行資料時出現髒讀。

(3)不可重複讀(不可重複讀到同一個值)

假設有一個事務A會多次對某一條資料進行重複查詢,另外有兩個事務B和C也會對該條資料進行更新。且事務A在事務B或事務C提交後才能讀取到修改的資料,即不會髒讀。但即使如此也會出現另外一個問題—不可重複讀。

比如快取頁裡有一條資料原來的值是A值。此時事務A開啟後,第一次查詢這條資料,查詢到的是A值。接著事務B更新了這一條資料的值為B值,同時馬上提交事務B。然後事務A此時還有邏輯要處理沒提交,需要第二次去查詢這一條資料。結果事務A第二次查詢這一條資料時查到的是事務B修改過的B值。因為事務B已經提交了,所以這時事務A可以讀到事務B更新後的值。緊接著事務C再次更新這一條資料為C值,並且也馬上提交事務C。然後事務A此時還有邏輯要處理沒提交,需要第三次去查詢這一條資料。結果事務A第三次查詢這一條資料時查到的值為事務C修改過的C值。

一.這種場景會有什麼問題

要說沒問題也可以是沒問題的,畢竟事務B和事務C都提交後,事務A多次查詢都查到它們修改後的值。

但是要說有問題也可以是有問題的,因為事務A第一次查詢到的是A值,那麼在事務A執行的期間,如果需要多次查詢同一行資料,希望得到的都是同樣的一個值,也就是希望這一條資料的A值在它事務裡面是可以重複讀取的,而在這種場景下,明顯A值是不可重複讀的。

二.是否可以重複讀取決於想要的資料是什麼樣

事務A在執行期間:如果希望每次查詢都能查到其他已提交事務修改過的值,是不可重複讀。如果希望每次查詢都能查到同樣的值,不管其他事務提交,是可重複讀。

所以如果期望的是可重複讀,但是資料庫表現的是不可重複讀。讓事務執行期間多次查到的值不同,都是其他已提交事務修改過的值。那麼就可以認為資料庫有問題,這個問題就是"不可重複讀"問題。

三.總結

所謂不可重複讀,就是事務A多次查詢一條資料,每次讀到的值不一樣。這個過程中可能別的事務會修改這條資料,且修改後這些事務也提交了。在避免髒讀的前提下,也導致事務A每次查詢到的值都不一樣。

所謂髒寫,就是事務A和B沒提交的情況下,都修改同一條資料。結果其中事務A回滾了,把另外一個事務B修改的值也給撤銷了。所以髒寫就是事務A和事務B在沒提交的情況下修改同一個值。

所謂髒讀,就是事務A修改了一條資料的值,結果還沒提交。另外一個事務B就讀到了事務A修改的值,然後事務A卻又回滾了。那麼事務B再次讀該資料時就讀不到剛才讀到那個要修改的值了。所以髒讀就是事務B讀到了事務A修改某條資料後還沒提交的值。

(4)幻讀

事務A先傳送一條SQL語句要查詢一批資料出來。比如"select * from table where id > 10",然後一開始查詢出10條資料。接著別的事務B往表裡插入了幾條資料,而且事務B還提交了。然後事務A再次查詢,按照相同的SQL語句去查,結果查詢出12條資料。於是在事務A中就出現了一模一樣的SQL語句,第一次查詢是10條資料,第二次查詢是12條資料,這就是幻讀。

所謂幻讀,指的是一個事務用一樣的SQL多次查詢一批資料,結果每次查詢都會發現查到了一些之前沒看到過的資料。

(5)總結

髒寫、髒讀、不可重複讀、幻讀,都是因為多執行緒併發執行事務。每個執行緒都開啟一個事務,每個事務都會執行增刪改查操作。然後資料庫在併發執行多個事務時,多個事務可能會併發對快取頁裡的同一批資料進行增刪改查操作。於是併發增刪改查同樣資料,就會導致髒寫、髒讀、不可重複讀、幻讀。髒寫、髒讀、不可重複讀、幻讀的本質就是資料庫的多事務併發問題。MySQL為了解決該問題使用了事務隔離、MVCC多版本隔離、鎖等機制。

4.SQL標準中對事務的4個隔離級別

SQL標準的事務隔離級別,並不是MySQL的事務隔離級別。MySQL在具體實現事務隔離級別時會有點差別。SQL標準規定了4種事務隔離級別。規定多個事務併發執行時互相是如何隔離的,從而避免事務併發問題。

這4中級別包括:Read Uncommitted(讀未提交)、Read Committed(讀已提交)、Repeatable Read(可重複讀)、Serializable(序列化)。不同的隔離級別可以避免不同的事務併發問題。

(1)Read Uncommitted隔離級別—讀未提交

不允許發生髒寫,可以去讀一些事務裡未提交的資料。這種隔離級別下,兩個事務不能改未提交。但是可能發生髒讀、不可重複讀、幻讀。一般來說,不會把事務隔離級別設定為讀未提交。

(2)Read Committed隔離級別—讀已提交

不會發生髒寫和髒讀,只能讀事務中已提交的資料。這種隔離級別下,是無法讀取事務在沒提交情況下修改的值。但是可能發生不可重複讀、幻讀,該隔離級別的簡稱是RC。把事務隔離級別設定成RC指的就是設定讀已提交級別。

(3)Repeatable Read隔離級別—可重複讀

對同一行資料,在事務中隨時可重複讀出同樣的值。這種隔離級別下,不會發生髒寫、髒讀和不可重複讀,但可能發生幻讀。該隔離級別簡稱RR,把事務隔離級別設定成RR指的是設定可重複讀。RR隔離級別,只保證對同一行資料的多次查詢不會被讀到不一樣的值。

(4)Serializable隔離級別—序列化執行

不允許多個事務併發執行。這種隔離級別下,多個事務只能序列起來執行。所以不會出現髒寫、髒讀、不可重複讀、幻讀的問題。一般來說,也不會把事務隔離級別設定為序列化級別。

5.MySQL是如何支援4種事務隔離級別的

SQL標準下的4種事務隔離級別,平時用的比較多的是RC和RR兩種級別。在MySQL中也支援這4中事務隔離級別。MySQL預設的事務隔離級別是RR級別,且MySQL的RR級別可避免幻讀。SQL標準裡的RR級別是會發生幻讀的,但MySQL的RR級別避免了幻讀。所以MySQL事務預設不會發生髒寫、髒讀、不可重複讀和幻讀的問題。

MySQL事務的執行都是並行的,各個事務互相不影響。事務A執行中出現事務B,事務A不會讀到事務B未提交的修改值。即使事務B提交了修改值事務A也不會讀到。即使事務B提交了插入的一行值事務A也依然不會讀到。

MySQL為了實現這種事務之間互不影響的效果,使用的是MVCC機制—多版本併發控制隔離機制。依託MVCC機制,MySQL就可以讓RR級別避免不可重複讀和幻讀的問題。MySQL的預設事務隔離級別是RR,一般不需修改。

6.Spring事務註解如何設定隔離級別

假設在開發業務系統時用Spring的@Transactional註解來做事務處理,如果要設定事務處理成RC級別,那麼可透過isolation引數進行設定。具體的設定方式如下:

@Transactional(isolation=Isolation.DEFAULT) ===> @Transactional(isolation=Isolation.READ_COMMITTED)

當設定為Isolation.DEFAULT時,就是MySQL預設事務隔離級別RR級別。不過需要注意的是,其實預設的RR隔離機制就挺好的,沒必要去修改。除非事務執行期間多次查詢時,業務必須要查到已提交事務修改過的值。那麼此時可設為Isolation.READ_COMMITTED級別。

7.uodo log多版本鏈介紹

MySQL預設的RR隔離級別,不會出現髒寫、髒讀、不可重複讀、幻讀。每個事務執行時,跟別的事務是沒有關係的。不管別的事務怎麼更新和插入,查到的值都是不變、都是一致的。而這就是由經典的MVCC多版本併發控制機制實現的。

MySQL每條資料都有兩個隱藏欄位:一個是trx_id,一個是roll_pointer。trx_id就是最近一次更新這條資料的事務id。roll_pointer就是指向更新這個事務之前生成的undo log。

假設有一個事務A(id=50)插入了一條資料,則此時這條資料的隱藏欄位及指向的undo log如下圖示。插入的這條資料值是A,因為事務A的id是50,故這條資料的trx_id=50。因為之前沒有這條資料,所以roll_pointer沒有指向任何執行的undo log。

MySQL原理簡介—8.MySQL併發事務處理

接著假設有一個事務B修改這條資料,把值改成了值B,事務B的id是58。那麼此時更新之前會生成一個undo log記錄之前的值,然後會讓roll_pointer指向這個實際的undo log回滾日誌。事務B修改完成後,此時表裡這行的資料值是B。trx_id是事務B的id=58,roll_pointer指向了undo log。指向的undo log記錄的是更新之前的資料值。

MySQL原理簡介—8.MySQL併發事務處理

接著繼續假設事務C又來修改一下這個值為值C,它的事務id是69。此時會把資料行裡的trx_id改成69,然後生成一條undo log。該undo log記錄之前事務B修改的那個值,如下圖示:

MySQL原理簡介—8.MySQL併發事務處理

綜上可知,多個事務序列執行時:每個事務的修改,都會更新隱藏欄位trx_id和roll_pointer。同時多個資料快照對應的undo log,會透過roll_pointer指標串聯起來。從而形成一個重要的undo log版本鏈。

8.基於undo log多版本鏈實現ReadView機制

(1)ReadView的關鍵內容

(2)基於undo log多版本鏈實現ReadView機制

(1)ReadView的關鍵內容

MySQL執行一個事務時會生成一個ReadView,裡面關鍵的內容有4個:

一.m_ids,表示此時有哪些事務在執行但還沒提交的,即活躍的事務;

二.min_trx_id,表示m_ids裡最小的值;

三.max_trx_id,表示MySQL下一個要生成的事務ID,最大事務ID;

四.creator_trx_id,表示當前這個事務自己的id;

(2)基於undo log多版本鏈實現ReadView機制

一.時間點一

假設原來資料庫有一行資料,一開始插入這行資料的事務id是32;

MySQL原理簡介—8.MySQL併發事務處理

接著有兩個事務併發執行,一是事務A(id=50),二是事務B(id=59)。事務A是去讀取這一行的資料值的,事務B是去更新這一行的資料值的。

二.時間點二

現在事務A建立一個ReadView,這個ReadView裡的m_ids就包含了事務A和事務B的兩個id:50和59。然後min_trx_id是50,max_trx_id是60,creator_trx_id是50(事務A的id)。這時事務A第一次查詢這行資料會進行一個判斷:看當前這行資料的trx_id是否小於ReadView中的min_trx_id。

此時發現trx_id=32,小於ReadView裡的min_trx_id=50。這說明事務A開啟事務前,修改這行資料的事務已提交,可查這行資料。

MySQL原理簡介—8.MySQL併發事務處理

三.時間點三

接著事務B開啟,把這行資料值修改為B,然後這行資料的trx_id設定為事務B的id=59。同時roll_pointer執行了修改之前生成的undo log,之後事務B進行了提交。

MySQL原理簡介—8.MySQL併發事務處理

這個時候事務A再次查詢時,會發現資料行裡的trx_id=59。大於ReadView裡的min_trx_id=50,小於ReadView裡的max_trx_id=60。這說明更新這條資料的事務,很可能是和事務A自己差不多同時開啟的。於是會看一下這個trx_id=59,是否在ReadView的m_ids列表裡。

最後發現在ReadView的m_ids列表裡,有50和59這兩個事務id。這就證明了事務B是在事務A開啟事務時併發執行提交的,所以這行修改後的資料對事務A來說是不能查的:

MySQL原理簡介—8.MySQL併發事務處理

既然trx_id=59的這一行資料不能查詢,那麼可以從這條資料的roll_pointer順著undo log日誌鏈往下找。於是就會找到最近的一條undo log,該undo log的trx_id=32。

此時發現trx_id=32,是小於ReadView裡的min_trx_id=50的。說明這個undo log版本必然是在事務A開啟之前就執行且提交的。

這就是undo log多版本鏈條的作用。它可以儲存一個快照鏈條,讓事務可以讀到之前的快照值。

MySQL原理簡介—8.MySQL併發事務處理

四.時間點四

接著假設事務A更新了這行資料的值為A,然後這行資料的trx_id修改為50,同時儲存之前事務B修改的值的快照。如下圖示:

MySQL原理簡介—8.MySQL併發事務處理

此後事務A來查詢這行資料的值,發現trx_id=50。和自己ReadView裡的creator_trx_id=50是一樣的,這說明這行資料是自己修改的,因此可以讀取它的值A。

五.時間點五

接著在事務A執行的過程中,突然開啟一個事務C,這個事務的id=78。然後事務C更新了這行資料的值為C,並且進行了提交。

這時事務A再去查詢這行資料,會發現當前資料的trx_id=78,大於自己ReadView中的max_trx_id=60,這說明事務C是事務A開啟後提交更新資料的。此時事務C更新的資料對於事務A而言是不能讀取的,於是事務A順著undo log多版本鏈條繼續往下找,自然就會找到事務A之前修改過的那個版本trx_id=50進行讀取。

MySQL原理簡介—8.MySQL併發事務處理

(3)總結

透過undo log多版本鏈條,加上事務開啟時建立的一個ReadView。當有查詢時,事務就能根據ReadVIew機制判斷應讀取哪個版本的資料,這個ReadVIew機制可以確保一個事務只能讀到:它自己開啟前其他事務進行更新並已經提交的值,以及它自己更新的值。

假如事務開啟前,有其他的事務已經正在執行,那麼當事務開啟後,其他事務更新了值並已提交,這時該事務讀取不到那些事務更新的值。

假如事務開啟後,才有其他事務開啟並更新了值以及進行了提交,那麼這時該事務也是讀取不到那些事務更新的值。

透過這個ReadView機制就可以實現多個事務併發執行時的資料隔離。

9.RC隔離級別如何基於ReadView機制實現

(1)RC隔離級別與ReadView機制

(2)基於ReadView機制實現RC隔離級別

(1)RC隔離級別與ReadView機制

一.RC隔離級別就是Read Committed讀已提交的隔離級別

指的是一個事務在執行期間,只要別的事務修改資料並且提交了。那麼這個事務就可以讀取到別的事務修改的資料,所以RC隔離是會發生不可重複讀、幻讀的問題。

二.ReadView機制是基於undo log版本鏈條實現的一套讀檢視機制

指的是事務開啟時生成一個ReadVIew:如果值是事務本身更新的,是可以讀取到的;如果值是在事務生成ReadView之前提交的事務修改的,也可以讀取;如果值是生成ReadView後再開啟事務修改並提交的,則是讀取不到的。

三.基於ReadView機制來實現RC隔離級別的核心

設定RC隔離級別的一個事務,每次發起查詢都重新生成一個ReadView。

(2)基於ReadView機制實現RC隔離級別

一.時間點一

假設資料庫裡有一行資料,插入時事務id=50,現在活躍著兩個事務:事務A和事務B,其id分別是60和70,活躍是指它們都還沒提交。

MySQL原理簡介—8.MySQL併發事務處理

二.時間點二

現在事務B發起一次更新操作,更新了這一行資料的值為B。這行資料的trx_id變為事務B的id=70,同時生成一條undo log。然後這行資料的roll_pointer會指向這新生成的undo log。

MySQL原理簡介—8.MySQL併發事務處理

這時事務A要發起一次查詢操作,就會生成一個ReadView。該ReadView的min_trx_id=60,max_trx_id=71,creator_trx_id=60。

MySQL原理簡介—8.MySQL併發事務處理

事務A發起查詢,發現這一行資料當前的trx_id=70,而trx_id=70屬於ReadView的活躍事務id範圍之間。這說明事務A在生成其ReadView之前就已存在id=70的活躍事務B。然後這個id=70的事務B修改了這條資料的值,但此時事務B還沒提交。因為在事務A的ReadView的m_ids活躍事務列表裡,有[60,70]兩個id。所以根據ReadView機制,此時事務A無法查到事務B修改的值B。

接著事務A就順著undo log版本鏈條往下查詢。於是會找到一個原始值,發現它的trx_id=50,小於min_trx_id=60。說明這個原始值在事務A生成ReadView之前,就由某事務插入並提交,因此事務A是可以查詢到這個原始值的。

MySQL原理簡介—8.MySQL併發事務處理

三.時間點三

接著假設事務B此時提交了,事務一旦提交就不會活躍於資料庫了。按照RC定義,事務B一旦提交,事務A再次查詢就能讀到事務B修改的值。為了讓事務A能夠讀到提交的事務B修改過的值,可以讓事務A發起查詢時,再次生成一個ReadView。比如事務B提交後事務A再次發起查詢,資料庫內活躍的事務只有事務A。因此min_trx_id=60,max_trx_id=71,m_ids=[60]。

MySQL原理簡介—8.MySQL併發事務處理

此時事務A再次基於這個新ReadView去查詢,會發現資料的trx_id=70。由於70在min_trx_id=60和max_trx_id=71範圍,但不在m_ids=[60]內。所以說明事務B在事務A生成這個新ReadView之前就已經提交了,因此事務A這次查詢可以查到事務B修改過的這個值。

MySQL原理簡介—8.MySQL併發事務處理

(3)總結

實現RC隔離級別的關鍵點在於事務每次查詢時都生成新的ReadView。如果一個事務在這次查詢之前,有其他事務修改了資料而且還提交了。那麼其生成ReadView的m_ids列表,當然就不包含這個已提交的事務。既然不包含已提交的事務,那麼就可以讀取到已提交事務修改過的值,以上就是基於ReadView實現的RC隔離級別的原理。

基於undo log多版本鏈條 + ReadView機制實現的RC + RR隔離級別,就是資料庫MVCC多版本併發控制機制。

10.RR隔離級別如何基於ReadView機制實現

(1)MySQL的RR隔離級別避免不可重複讀+幻讀

(2)基於ReadView機制實現避免不可重複讀

(3)基於ReadView機制實現避免幻讀

(1)MySQL的RR隔離級別避免不可重複讀+幻讀

MySQL的RR級別下,一個事務讀一條資料,無論讀多少次都是一個值。其他事務修改資料後哪怕提交了,該事務也無法看到其他事務修改的值。同時如果其他事務插入了一些新的資料,該事務也是讀取不到。這樣就分別避免了出現不可重複讀的問題,以及避免了出現幻讀的問題。

(2)基於ReadView機制實現避免不可重複讀

一.時間點一

假設有一條資料是事務id=50插入的,此時有事務A和事務B同時執行,事務A的id=60,事務B的id=70。這時事務A發起一個查詢,第一次查詢會生成一個ReadView。此時ReadView如下圖示:

MySQL原理簡介—8.MySQL併發事務處理

這時事務A基於這個ReadView去查這條資料,會發現這條資料的trx_id=50,小於ReadView裡的min_trx_id。說明在事務A發起查詢之前,早就有其他事務插入這條資料並且提交了。所以事務A此時是可以查到這條資料的原始值的。

MySQL原理簡介—8.MySQL併發事務處理

二.時間點二

接著事務B更新這條資料的值為B和trx_id=70,同時生成一個undo log。而且事務B也進行提交了,所以此時事務B已結束。

在RR隔離級別下,這時事務A的ReadView中的m_ids還是60和70。因為RR隔離級別下,ReadView一旦生成了就不會改變了。於是事務A繼續去查詢這條資料的值,會發現資料的trx_id=70。雖然70在ReadView的min_trx_id和max_trx_id的範圍,但也在m_ids中。這就說明事務A開啟時,id為70的這個事務B還在執行的。然後由id為70的事務B更新了資料,所以此時事務A不能讀取更新的值。

接著事務A順著這條資料的roll_pointer指標找到下面一條資料,發現該資料的trx_id=50,小於事務A的ReadView的min_trx_id。說明事務A開啟之前,已提交該id為50的事務,所以可讀取其值。

MySQL原理簡介—8.MySQL併發事務處理

三.基於ReadView機制實現避免不可重複讀的總結

事務A多次讀同一個資料,每次讀到的都是一樣的值。除非是它自己修改了值,否則讀到的都是一樣的值。不管別的事務如何修改資料,事務A的ReadView始終是不變的。所以事務A基於這個ReadView,可以讀取到的值始終是一樣的。

(3)基於ReadView機制實現避免幻讀

一.時間點一

假設事務A繼續用"select * from x where id > 10"來查詢,此時查到的只有一條資料,而且讀到的是這條資料的原始值版本。

MySQL原理簡介—8.MySQL併發事務處理

二.時間點二

現在有一個事務C插入了一條資料,然後提交了。接著事務A再次查詢,會發現符合條件的有2條資料。一條是原始值那個資料,一條是事務C插入的那條資料。事務C插入的資料的trx_id=80,大於事務A的ReadView的max_trx_id。說明事務C是事務A開啟後才啟動的,所以這條資料不能查詢。

因此事務A本次查詢,還是隻能查到原始值這條資料。由此可見事務A不會發生幻讀,因為它根據條件範圍查詢的時候。每次讀到的資料都是一樣的,不會讀到其他事務新增的資料。

MySQL原理簡介—8.MySQL併發事務處理

11.多事務併發執行的隔離機制總結

(1)多個事務併發執行讀寫同一資料時的問題

(2)事務隔離級別:RU、RC、RR和序列化

(3)MySQL的MVCC機制

(1)多個事務併發執行讀寫同一資料時的問題

一.髒寫

事務A和B更新同一資料,

事務A回滾時把事務B的更新也回滾了;

二.髒讀

事務A先讀事務B沒提交時修改的資料,

然後事務B回滾後再讀就讀不到;

三.不可重複讀

事務A多次讀同一資料,

其他事務修改資料並提交,於是讀到不同的值;

四.幻讀

每次範圍查詢查到的資料不同,

其他事務插入新值,就會讀到更多資料;

(2)事務隔離級別:RU、RC、RR和序列化

一.RU隔離級別

可以讀到其他事務未提交的修改資料,

只能避免髒寫;

二.RC隔離級別

可以讀到其他事務已提交的修改資料,

可以避免髒寫和髒讀;

三.RR隔離級別

不會讀到其他事務已提交的修改資料,

可以避免髒寫、髒讀和不可重複讀;

四.序列隔離級別

指的是讓事務都序列執行,

可以避免所有問題;

(3)MySQL的MVCC機制

MySQL中多事務併發執行的隔離原理,就是MVCC機制——multi version concurrent control,專門控制多個事務併發執行時,互相之間會如何影響。

基於undo log多版本鏈條 + ReadView機制,可實現MySQL的MVCC。MySQL預設的RR隔離級別,就是基於這套機制來實現的。除了避免髒寫、髒讀、不可重複讀,還能避免幻讀問題。因此一般來說,MySQL使用預設的RR隔離級別即可。

12.多事務更新同一行資料如何加鎖避免髒寫

依靠鎖機制讓多個事務更新一行資料時序列化,避免同時更新一行資料。

一.時間點一

在MySQL裡,假設有一行資料暫時沒有被任何事務處理。此時有一個事務A要來更新這行資料,首先會看這行資料是否被加上鎖。該事務發現這一行資料並沒有加鎖,於是就會建立一個鎖。這個鎖包含了這個事務的trx_id=50和等待狀態=false。該事務A建立完一個鎖之後,會將鎖和這行資料關聯在一起。

由於更新一行資料要把對應資料頁從磁碟檔案讀取到快取頁才能更新,所以此時這一行資料和關聯的鎖資料結構,都是在記憶體裡的。

MySQL原理簡介—8.MySQL併發事務處理

二.時間點二

因為事務A給這行資料加了鎖,此時就不能被其他事務訪問了。這時有另外一個事務B過來了,這個事務B也想更新這一行資料。此時事務B會先檢查一下,當前這行資料是否被加鎖了。

結果事務B發現這行資料已被加鎖,於是會生成一個鎖資料結構進行排隊。這個鎖的資料結構會有事務B的trx_id=60和等待狀態=true。

MySQL原理簡介—8.MySQL併發事務處理

三.時間點三

接著事務A這時更新完資料,就會把自己的鎖給釋放掉。鎖釋放掉後MySQL就會找其他對這行資料加鎖的事務,於是找到事務B。事務B就會把鎖裡的等待狀態修改為false,然後喚醒事務B繼續執行。此時事務B就獲取到鎖了:

MySQL原理簡介—8.MySQL併發事務處理

13.共享鎖和獨佔鎖是什麼

(1)獨佔鎖讓多事務對同一行資料寫寫操作互斥

(2)MVCC機制避免對同一行資料讀寫操作加鎖

(3)共享鎖讓多事務對同一行資料讀寫操作互斥

(4)查詢時加獨佔鎖

(1)獨佔鎖讓多事務對同一行資料寫寫操作互斥

多個事務同時更新一行資料時,每個事務都會加鎖,然後都會排隊等待。必須等事務執行完畢提交了並釋放了鎖,才能喚醒其他事務繼續執行。這個過程中,多個事務更新時所加的鎖,就是獨佔鎖——X鎖。

(2)MVCC機制避免對同一行資料讀寫操作加鎖

此外當有事務更新資料時,其他的事務是可以讀取該資料的。但讀取該資料的事務是不用加鎖的,只有更新該資料的事務才需要加鎖。預設情況會開啟MVCC機制,讓事務更新資料時其他事務能讀取該資料。所以一行資料的讀和寫兩個操作,預設不會加鎖互斥。

MySQL透過MVCC機制來實現避免頻繁加鎖互斥。一個事務讀取資料時,完全可以根據該事務建立的ReadView內容,去undo log版本鏈找一個能讀取的版本,不用考慮其他事務的併發修改。

(3)共享鎖讓多事務對同一行資料讀寫操作互斥

如果希望事務在執行查詢操作的時候也加鎖,那麼MySQL也可以支援。MySQL支援一種共享鎖——S鎖,這個共享鎖的語法如下:

mysql> select * from table lock in share mode;

只需要在一個查詢語句後面加上lock in share mode,就表示查詢時需要對一行資料加共享鎖。

需要注意的是,共享鎖和獨佔鎖是互斥的。也就是說,如果有事務正在更新一行資料,已經加了獨佔鎖,就不能對查詢這行資料的事務也加共享鎖。同樣道理,如果有事務對一行資料先加了共享鎖,其他事務也不能來更新加獨佔鎖。

需要注意的是,共享鎖和共享鎖是不會互斥的。如果一個事務給一行資料加了共享鎖,其他事務也可以對該行資料繼續加共享鎖。

(4)查詢時加獨佔鎖

MySQL的查詢操作還可以加互斥鎖,語法如下是:

mysql> select * from table for update;

這樣查詢時就會加上獨佔鎖,直到事務提交後,其他事務才能更新資料。

(5)總結

更新資料時必然加獨佔鎖,獨佔鎖和獨佔鎖是互斥的,此時其他事務不能更新。

進行查詢時預設是不加鎖的,會透過MVCC機制讀快照版本,但查詢可以手動加共享鎖和獨佔鎖。

共享鎖和獨佔鎖是互斥的,但共享鎖和共享鎖不互斥。查詢時手動加共享鎖:select * from table lock in share mode;查詢時手動加獨佔鎖:select * from table for update;

一般開發業務系統時,其實很少會在查詢時主動加共享鎖。通常基於Redis和Zookeeper的分散式鎖來控制業務系統的鎖邏輯。

14.DDL操作是否會導致表級別加鎖

(1)行鎖中的獨佔鎖和共享鎖總結

(2)DDL操作透過後設資料鎖實現類似表鎖的效果

(1)行鎖中的獨佔鎖和共享鎖總結

在多個事務併發更新資料時,都是在行級別加獨佔鎖的,這就是行鎖。獨佔鎖都是互斥的,所以不可能發生髒寫問題。一個事務提交了才會釋放自己的獨佔鎖,並喚醒下一個事務執行。

如果有事務去讀取別的事務正在更新的資料,有兩種可能:第一種可能是基於MVCC機制進行事務隔離,讀取快照版本,比較常見。第二種可能是查詢的同時基於特殊語法去加獨佔鎖或者共享鎖。

如果查詢時加獨佔鎖,那麼會和其他更新資料的事務加的獨佔鎖互斥。

如果查詢時加共享鎖,那麼不會和其他查詢加的共享鎖互斥。但和其他更新資料的事務加的獨佔鎖互斥,也和其他查詢資料的事務加的獨佔鎖互斥。

一般而言,不建議在資料庫粒度去透過行鎖實現複雜的業務鎖機制。而應該透過Redis、Zookeeper使用分散式鎖來實現複雜業務的鎖機制。

預設情況下,多個事務併發執行更新一條資料,是加獨佔鎖的。而其他事務讀取資料則基於MVCC機制進行快照版本讀,實現事務隔離。

(2)DDL操作透過後設資料鎖實現類似表鎖的效果

在資料庫裡,不僅可以透過查詢中的特殊語法加行鎖。比如lock in share mode、for update等,還可以透過一些方法在表級別上加鎖。

如下說法有一定道理:執行增刪改時預設加行鎖,執行DDL語句時預設在表級別加鎖。因為執行DDL時會阻塞所有增刪改操作,而執行增刪改操作時會阻塞DDL操作。

但實際上這是透過MySQL的通用後設資料鎖來實現的,也就是Metadata Locks,但這不是表鎖的概念。因為表鎖是InnoDB儲存引擎的概念,InnoDB提供了自己的表級鎖。InnoDB的表級鎖和DDL語句裡的後設資料鎖不是一個概念,只不過DDL語句和增刪改操作,確實是互斥的。

15.表鎖和行鎖相互之間的關係以及互斥規則

(1)表鎖分為表級共享鎖、表級獨佔鎖

(2)表級的意向鎖分為意向獨佔鎖、意向共享鎖

(1)表鎖分為表級共享鎖、表級獨佔鎖

MySQL的表鎖,其實是相當雞肋,幾乎很少會用到。表鎖分為兩種,一種是表鎖,另一種是表級的意向鎖。

可以使用如下語法來加:

LOCK TABLES xxx READ:加表級共享鎖;

LOCK TABLES xxx WRITE:加表級獨佔鎖;

一般來說,幾乎不會用這兩個語法去加表鎖。

(2)表級的意向鎖分為意向獨佔鎖、意向共享鎖

在下面這兩種情況下會加表級意向鎖:

事務執行增刪改操作,除了在行級加獨佔鎖之外,還會在表級加一個意向獨佔鎖;

事務執行查詢操作,會在表級加一個意向共享鎖;

平時資料庫操作中,比較常見的兩種表鎖:就是更新和查詢操作時自動加的意向獨佔鎖和意向共享鎖,但這兩種意向鎖之間是不會互斥的。

但更新資料時自動加的表級意向獨佔鎖,會和"LOCK TABLES xxx WRITE"手動加的表級獨佔鎖互斥,也和"LOCK TABLES xxx READ"手動加的表級共享鎖互斥。

查詢資料時自動加的表級意向共享鎖,只會和"LOCK TABLES xxx WRITE"手動加的表級獨佔鎖互斥。

一般來說,根本就不會手動加表級鎖。讀寫操作時自動加的表級意向鎖相互之間是不互斥的。

對同一行資料的更新操作加的行級獨佔鎖,和讀操作是不互斥的。因為讀操作不加鎖,預設是透過MVCC機制來讀取快照版本的。

16.資料庫出現不確定的效能抖動的原因

(1)資料庫出現週期性抖動的問題

(2)資料庫執行更新語句的流程分析

(3)效能抖動的可能情況一

(4)效能抖動可能的情況二

(5)線上資料庫效能抖動原因總結

(1)資料庫出現週期性抖動的問題

線上資料庫時不時莫名其妙的來一次效能抖動,而且造成效能抖動的不是資料庫鋰電池充放電的問題。

(2)資料庫執行更新語句的流程分析

資料庫執行更新語句時,都是先從磁碟上載入資料頁到記憶體的快取頁裡。然後會更新快取頁,同時寫對應的redo log日誌到Redo Log Buffer中。

MySQL原理簡介—8.MySQL併發事務處理

既然更新了Buffer Pool裡的快取頁,快取頁就會變成髒頁。因為此時快取頁裡的資料和磁碟檔案裡的資料頁的資料不一樣。對於髒頁,需要有一個合適的時機把資料刷入到磁碟裡,資料庫會維護一個LRU連結串列和一個flush連結串列來實現。

如果載入磁碟檔案的資料頁到Buffer Pool時發現並沒有空閒的快取頁,此時就必須把部分髒頁刷入到磁碟檔案裡。於是MySQL會根據LRU連結串列尋找最近最少被訪問的快取頁刷入磁碟,當然MySQL在不那麼繁忙時也會從flush連結串列將一部分髒頁刷入磁碟。

MySQL原理簡介—8.MySQL併發事務處理

(3)效能抖動的可能情況一

要執行的一個查詢語句需要查詢大量資料頁並載入到快取頁裡,此時就可能導致記憶體裡大量的髒頁需要淘汰出去然後刷入磁碟,這樣才能騰出足夠的記憶體空間來執行這條查詢語句。

在這種情況下,可能就會出現資料庫在執行某個查詢語句時效能抖動。平時只有幾十毫秒的查詢,這次需要幾秒。就是因為要等待大量髒頁刷入到磁碟,才能載入查詢出的大量資料頁,然後SQL語句才能執行,所以才會導致耗時突增。

(4)效能抖動可能的情況二

執行大量更新寫滿所有redo日誌檔案,且還不能覆蓋第一個日誌檔案。因為往redo日誌檔案寫入redo log太快了,都追上checkpoint檢查點了。此時只能讓髒頁刷盤,讓一些redo log失效來騰出redo日誌檔案的空間。

一.redo log刷盤的時機

Redo Log Buffer裡的資料超過容量的一半 + 提交事務。這兩種情況都會強制將Redo Log Buffer裡的Redo Log Block刷入磁碟上的redo日誌檔案中。

二.髒頁刷盤的時機

所有redo日誌檔案都被寫滿時,會觸發一次髒頁的刷盤。磁碟上會有多個redo日誌檔案,這些redo日誌檔案會迴圈不停地寫入。如果所有redo日誌檔案都寫滿了,此時會回到第一個redo日誌檔案寫入。

三.所有redo日誌檔案寫滿時要判斷第一個redo日誌檔案能否被覆蓋

如果第一個redo日誌檔案裡靠前的一些redo日誌,所對應Buffer Pool快取頁的資料,還沒有被重新整理到磁碟檔案的資料頁中。一旦把這個redo日誌檔案裡的redo日誌進行覆蓋,此時資料庫卻崩潰了,那麼被覆蓋的redo日誌和它對應的被更新過的快取頁資料就徹底丟失了。

所以當所有redo日誌檔案寫滿,需要從第一個redo日誌檔案開始寫時,就會判斷第一個日誌檔案裡靠前的redo log對應的快取頁是否已刷盤。如果是,則要把要被覆蓋的redo log對應的快取頁馬上刷入磁碟。

MySQL原理簡介—8.MySQL併發事務處理

四.寫滿redo log的所有日誌檔案時發現不能覆蓋第一個redo日誌檔案

此時就需要把第一個redo日誌檔案裡靠前的一些redo log,所對應Buffer Pool中沒被刷入磁碟的快取頁(髒頁),都刷入到磁碟。從而導致資料庫無法處理任何更新請求,因為更新請求需要寫redo log。而此時還在等待髒頁被重新整理到磁碟,才能有可以覆蓋的redo日誌檔案。之後才能執行更新語句,才能把新的redo log寫入第一個redo日誌檔案。

五.如果某時刻MySQL在執行大量的更新語句

那麼可能會發現資料庫的很多更新語句突然短時間內效能抖動了,可能很多幾毫秒就執行完的更新語句,這時卻要等1s才能執行完畢。

其中的原因大機率就是,所有redo日誌檔案寫滿了。必須要等第一個redo日誌檔案裡部分redo log對應的髒頁都刷入磁碟,才能繼續執行更新語句,讓其redo日誌能覆蓋到第一個redo日誌檔案中,從而導致此時執行的更新語句效能很差。

(5)線上資料庫效能抖動原因總結

導致資料庫的更新語句突然出現效能抖動,很可能是以下兩種情況。

情況一:要執行的一個查詢語句需要查詢大量資料頁並載入到快取頁裡,由於沒有足夠的空閒快取頁,需要等大量髒頁刷盤才能繼續載入資料頁。

情況二:執行大量更新語句導致所有redo日誌檔案寫滿且還不能覆蓋第一個檔案,要等第一個檔案裡部分redo log對應的髒頁刷盤才能繼續執行更新語句。

17.如何最佳化資料庫不確定性的效能抖動

(1)查詢和更新時出現效能抖動的可能原因

(2)如何最佳化引數減少髒頁刷盤帶來的效能抖動

(3)如何減少髒頁刷盤的時間

(1)查詢和更新時出現效能抖動的可能原因

上面分析了有時在資料庫執行查詢或者更新語句時,可能SQL語句效能會出現不正常的莫名奇妙的抖動,可能平時只需要幾十毫秒執行完成的卻居然需要幾秒鐘才能完成。這種莫名奇妙的效能抖動,在分析過底層原理後,根本原因就兩個。

原因一:執行查詢時Buffer Pool的快取頁滿了

當執行一個需要查詢很多資料的SQL時,需要把很多快取頁刷入磁碟。由於髒頁刷磁碟太慢了,於是就會導致查詢語句執行得很慢。因為要等很多快取頁都刷盤,才能把查詢需要的資料頁載入到快取頁中。

原因二:執行更新時磁碟上的所有redo日誌滿了

此時需要回到第一個redo log日誌檔案嘗試進行覆蓋寫,這又涉及第一個redo log日誌檔案裡很多redo log對應的快取頁還沒刷盤。所以此時就必須把那些快取頁刷入到磁碟,才能執行後續的更新語句,於是就會導致執行的更新語句很慢了。

(2)如何最佳化引數減少髒頁刷盤帶來的效能抖動

最佳化一:儘量減少髒頁刷盤的頻率

給資料庫採用大記憶體機器,給Buffer Pool分配更大的記憶體空間。那麼也只能讓快取頁被填滿的速度低一些,降低出現這種情況的頻率。

最佳化二:儘量提升髒頁刷盤的速度

假設現在要執行一個SQL查詢語句,此時要等待刷入一批快取頁到磁碟,接著才能載入資料到快取頁。

如果把那批快取頁刷入磁碟需要1s,然後查詢語句執行的時間是200ms,此時這條SQL執行完畢的總時間就需要1.2s。如果把那批快取頁刷入到磁碟的時間最佳化到100ms,然後再加上200ms,這條SQL執行完畢的總時間只要300ms,效能提升了。

所以關鍵點在於,儘可能將快取頁刷入到磁碟的時間開銷減到最小。

(3)如何減少髒頁刷盤的時間

一.採用SSD固態硬碟而不要使用機械硬碟

因為SSD固態硬碟最強大的地方,就是它的隨機IO效能非常高。而把快取頁刷入到磁碟,就是典型的隨機IO,需要在磁碟上找到各個快取頁所在的隨機位置,把資料寫入到磁碟裡去。所以如果採用SSD固態硬碟,那麼快取頁刷盤的效能就會提高不少。

二.設定以最大隨機IO速率刷盤

除了SSD外,還得設定一個關鍵的引數,就是innodb_io_capacity。這個引數告訴資料庫採用多大的IO速率把快取頁刷入到磁碟。

如果SSD能承載每秒600次隨機IO,但innodb_io_capacity只設定300。也就是把快取頁刷入到磁碟時,每秒最多執行300次隨機IO。那麼這樣就根本無法把SSD固態硬碟的隨機IO效能發揮出來。

所以通常建議對機器的SSD固態硬碟承載的最大隨機IO速率進行測試。可以使用fio工具來測試,測出磁碟最大的隨機IO速率。測出SSD固態硬碟的最大隨機IO速率後,就設定給innodb_io_capacity。這樣就可以儘可能讓資料庫用最大的速率去把快取頁刷入到磁碟。

三.設定禁止刷入鄰近的快取頁

還有一個關鍵引數,就是innodb_flush_neighbors。這個引數可以控制快取頁刷盤時,臨近的其他快取頁是否也刷入到磁碟。如果該引數設定為1,那麼就會導致每次刷入磁碟的快取頁太多了。

所以如果使用了SSD固態硬碟,並沒必要讓資料庫同時刷鄰近的快取頁。可將該引數設定為0,禁止刷鄰近快取頁,減少每次重新整理快取頁的數量。

(4)總結

針對MySQl效能隨機抖動的問題:最核心的就是把innodb_io_capacity設定為SSD固態硬碟的IOPS。同時設定innodb_flush_neighbors為0,禁止讓資料庫刷鄰近的快取頁。從而讓資料庫能儘快將快取頁刷進磁碟,及減少每次要刷快取頁的數量,最終將快取頁刷入磁碟的效能提到最高。

相關文章