一條簡單的更新語句,MySQL是如何加鎖的?


看以下一條sql語句:sql

# table T (id int, name varchar(20))delete from T where id = 10;複製代碼
MySQL在執行的過程當中,是如何加鎖呢?
在看下面這條語句:
select * from T where id = 10;複製代碼
那這條語句呢?其實這其中包含太多知識點了。要回答這兩個問題,首先須要瞭解一些知識。

相關知識介紹
性能優化

多版本併發控制
bash

在MySQL默認存儲引擎InnoDB中,實現的是基於多版本的併發控制協議——MVCC(Multi-Version Concurrency Control)(注:與MVVC相對的,是基於鎖的併發控制,Lock-Based Concurrency Control)。其中MVCC最大的好處是:讀不加鎖,讀寫不衝突。在讀多寫少的OLTP應用中,讀寫不衝突是很是重要的,極大的提升了系統的併發性能,在現階段,幾乎全部的RDBMS,都支持MVCC。其實,MVCC就一句話總結:同一份數據臨時保存多個版本的一種方式,進而實現併發控制。
微信

當前讀和快照讀
併發

在MVCC併發控制中,讀操做能夠分爲兩類:快照讀與當前讀。

快照讀(簡單的select操做):讀取的是記錄中的可見版本(多是歷史版本),不用加鎖。這你就知道第二個問題的答案了吧。app

當前讀(特殊的select操做、insert、delete和update):讀取的是記錄中最新版本,而且當前讀返回的記錄都會加上鎖,這樣保證了了其餘事務不會再併發修改這條記錄。

彙集索引性能

也叫作聚簇索引。在InnoDB中,數據的組織方式就是聚簇索引:完整的記錄,儲存在主鍵索引中,經過主鍵索引,就能夠獲取記錄中全部的列。

最左前綴原則學習

也就是最左優先,這條原則針對的是組合索引和前綴索引,理解:
一、在MySQL中,進行條件過濾時,是按照向右匹配直到遇到範圍查詢(>,<,between,like)就中止匹配,好比說a = 1 and b = 2 and c > 3 and d = 4 若是創建(a, b, c, d)順序的索引,d是用不到索引的,若是創建(a, b, d, c)索引就都會用上,其中a,b,d的順序能夠任意調整。
二、= 和 in 能夠亂序,好比 a = 1 and b = 2 and c = 3 創建(a, b, c)索引能夠任意順序,MySQL的查詢優化器會優化索引能夠識別的形式。

兩階段鎖優化

傳統的RDMS加鎖的一個原則,就是2PL(Two-Phase Locking,二階段鎖)。也就是說鎖操做分爲兩個階段:加鎖階段和解鎖階段,而且保證加鎖階段和解鎖階段不想交。也就是說在一個事務中,無論有多少條增刪改,都是在加鎖階段加鎖,在 commit 後,進入解鎖階段,纔會所有解鎖。

隔離級別ui

MySQL/InnoDB中,定義了四種隔離級別:
Read Uncommitted:能夠讀取未提交記錄。此隔離級別不會使用。
Read Committed(RC):針對當前讀,RC隔離級別保證了對讀取到的記錄加鎖(記錄鎖),存在幻讀現象。
Repeatable Read(RR):針對當前讀,RR隔離級別保證對讀取到的記錄加鎖(記錄鎖),同時保證對讀取的範圍加鎖,新的知足查詢條件的記錄不可以插入(間隙鎖),不存在幻讀現象。
Serializable:從MVCC併發控制退化爲基於鎖的併發控制。不區別快照讀和當前讀,全部的讀操做都是當前讀,讀加讀鎖(S鎖),寫加寫鎖(X鎖)。在該隔離級別下,讀寫衝突,所以併發性能急劇降低,在MySQL/InnoDB中不建議使用。

Gap鎖和Next-Key鎖

在InnoDB中完整行鎖包含三部分:
記錄鎖(Record Lock):記錄鎖鎖定索引中的一條記錄。
間隙鎖(Gap Lock):間隙鎖要麼鎖住索引記錄中間的值,要麼鎖住第一個索引記錄前面的值或最後一個索引記錄後面的值。
Next-Key Lock:Next-Key鎖時索引記錄上的記錄鎖和在記錄以前的間隙鎖的組合。

進行分析

瞭解完以上的小知識點,咱們開始分析第一個問題。當看到這個問題的時候,你可能會堅決果斷的說,加寫鎖啊。這答案也錯也對,由於已知條件太少。那麼有那些須要已知的前提條件呢?
  • 前提一:id列是否是主鍵?

  • 前提二:當前系統的隔離級別是什麼?

  • 前提三:id列若是不是主鍵,那麼id列上有沒有索引呢?

  • 前提四:id列上若是有二級索引,那麼是惟一索引嗎?

  • 前提五:SQL執行計劃是什麼?索引掃描?仍是全表掃描

根據上面的前提條件,能夠有九種組合,固然尚未列舉徹底。
  • id列是主鍵,RC隔離級別

  • id列是二級惟一索引,RC隔離級別

  • id列是二級不惟一索引,RC隔離級別

  • id列上沒有索引,RC隔離級別

  • id列是主鍵,RR隔離級別

  • id列是二級惟一索引,RR隔離級別

  • id列是二級不惟一索引,RR隔離級別

  • id列上沒有索引,RR隔離級別

組合一:id主鍵 + RC

這個組合是分析最簡單的,到執行該語句時,只須要將主鍵id = 10的記錄加上X鎖。以下圖所示:

結論:id是主鍵是,此SQL語句只須要在id = 10這條記錄上加上X鎖便可。

組合二:id惟一索引 + RC

這個組合,id不是主鍵,而是一個Unique的二級索引鍵值。在RC隔離級別下,是怎麼加鎖的呢?看下圖:
因爲id是Unique索引,所以delete語句會選擇走id列的索引進行where條件過濾,在找到id = 10的記錄後,首先會將Unique索引上的id = 10的記錄加上X鎖,同時,會根據讀取到的name列,回到主鍵索引(聚簇索引),而後將聚簇索引上的name = 'e' 對應的主鍵索引項加X鎖。
結論:若id列是Unique列,其上有Unique索引,那麼SQL須要加兩個X鎖,一個對應於id Unique索引上的id = 10的記錄,另外一把鎖對應於聚簇索引上的(name = 'e', id = 10)的記錄。

組合三:id不惟一索引+RC

該組合中,id列不在惟一,而是個普通索引,那麼當執行sql語句時,MySQL又是如何加鎖呢?看下圖:
由上圖能夠看出,首先,id列索引上,知足id = 10查詢的記錄,均加上X鎖。同時,這些記錄對應的主鍵索引上的記錄也加上X鎖。與組合er的惟一區別,組合二最多隻有一個知足條件的記錄,而在組合三中會將全部知足條件的記錄所有加上鎖。

結論:若id列上有非惟一索引,那麼對應的全部知足SQL查詢條件的記錄,都會加上鎖。同時,這些記錄在主鍵索引上也會加上鎖。

組合四:id無索引+RC

相對於前面的組合,該組合相對特殊,由於id列上無索引,因此在 where id = 10 這個查詢條件下,無法經過索引來過濾,所以只能全表掃描作過濾。對於該組合,MySQL又會進行怎樣的加鎖呢?看下圖:

因爲id列上無索引,所以只能走聚簇索引,進行全表掃描。由圖能夠看出知足條件的記錄只有兩條,可是,聚簇索引上的記錄都會加上X鎖。但在實際操做中,MySQL進行了改進,在進行過濾條件時,發現不知足條件後,會調用 unlock_row 方法,把不知足條件的記錄放鎖(違背了2PL原則)。這樣作,保證了最後知足條件的記錄加上鎖,可是每條記錄的加鎖操做是不能省略的。

結論:若id列上沒有索引,MySQL會走聚簇索引進行全表掃描過濾。因爲是在MySQl Server層面進行的。所以每條記錄不管是否知足條件,都會加上X鎖,可是,爲了效率考慮,MySQL在這方面進行了改進,在掃描過程當中,若記錄不知足過濾條件,會進行解鎖操做。同時優化違背了2PL原則。

組合五:id主鍵+RR
該組合爲id是主鍵,Repeatable Read隔離級別,針對於上述的SQL語句,加鎖過程和組合一(id主鍵+RC)一致。

組合六:id惟一索引+RR

該組合與組合二的加鎖過程一致。

組合七:id不惟一索引+RR

在組合一到組合四中,隔離級別是Read Committed下,會出現幻讀狀況,可是在該組合Repeatable Read級別下,不會出現幻讀狀況,這是怎麼回事呢?而MySQL又是如何給上述語句加鎖呢?看下圖:
該組合和組合三看起來很類似,但差異很大,在改組合中加入了一個間隙鎖(Gap鎖)。這個Gap鎖就是相對於RC級別下,RR級別下不會出現幻讀狀況的關鍵。實質上,Gap鎖不是針對於記錄自己的,而是記錄之間的Gap。所謂幻讀,就是同一事務下,連續進行屢次當前讀,且讀取一個範圍內的記錄(包括直接查詢全部記錄結果或者作聚合統計), 發現結果不一致(標準檔案通常指記錄增多, 記錄的減小應該也算是幻讀)。

那麼該如何解決這個問題呢?如何保證屢次當前讀返回一致的記錄,那麼就須要在多個當前讀之間,其餘事務不會插入新的知足條件的記錄並提交。爲了實現該結果,Gap鎖就應運而生。

如圖所示,有些位置能夠插入新的知足條件的記錄,考慮到B+樹的有序性,知足條件的記錄必定是具備連續性的。所以會在 [4, b], [10, c], [10, d], [20, e] 之間加上Gap鎖。
Insert操做時,如insert(10, aa),首先定位到 [4, b], [10, c]間,而後插入在插入以前,會檢查該Gap是否加鎖了,若是被鎖上了,則Insert不能加入記錄。所以經過第一次當前讀,會把知足條件的記錄加上X鎖,還會加上三把Gap鎖,將可能插入知足條件記錄的3個Gap鎖上,保證後續的Insert不能插入新的知足 id = 10 的記錄,也就解決了幻讀問題。

而在組合五,組合六中,一樣是RR級別,可是不用加上Gap鎖,在組合五中id是主鍵,組合六中id是Unique鍵,都能保證惟一性。一個等值查詢,最多隻能返回一條知足條件的記錄,並且新的相同取值的記錄是沒法插入的。

結論:在RR隔離級別下,id列上有非惟一索引,對於上述的SQL語句;首先,經過id索引定位到第一條知足條件的記錄,給記錄加上X鎖,而且給Gap加上Gap鎖,而後在主鍵聚簇索引上知足相同條件的記錄加上X鎖,而後返回;以後讀取下一條記錄重複進行。直至第一條出現不知足條件的記錄,此時,不須要給記錄加上X鎖,可是須要給Gap加上Gap鎖嗎,最後返回結果。

組合八:id無索引+RR

該組合中,id列上無索引,只能進行全表掃描,那麼該如何加鎖,看下圖:
如圖,能夠看出這是一個很恐怖的事情,全表每條記錄要加X鎖,每一個Gap加上Gap鎖,若是表上存在大量數據時,又是什麼情景呢?這種狀況下,這個表,除了不加鎖的快照讀,其餘任何加鎖的併發SQL,均不能執行,不能更新,刪除,插入,這樣,全表鎖死。

固然,和組合四同樣,MySQL進行了優化,就是semi-consistent read。semi-consistent read開啓的狀況下,對於不知足條件的記錄,MySQL會提早放鎖,同時Gap鎖也會釋放。而semi-consistent read是如何觸發:要麼在Read Committed隔離級別下;要麼在Repeatable Read隔離級別下,設置了 innodb_locks_unsafe_for_binlog 參數。


結論:在Repeatable Read隔離級別下,若是進行全表掃描的當前讀,那麼會鎖上表上的全部記錄,而且全部的Gap加上Gap鎖,杜絕全部的 delete/update/insert 操做。固然在MySQL中,能夠觸發 semi-consistent read來緩解鎖開銷與併發影響,可是semi-consistent read自己也會帶來其餘的問題,不建議使用。

組合九:Serializable

在最後組合中,對於上訴的刪除SQL語句,加鎖過程和組合八一致。可是,對於查詢語句(好比select * from T1 where id = 10)來講,在RC,RR隔離級別下,都是快照讀,不加鎖。在Serializable隔離級別下,不管是查詢語句也會加鎖,也就是說快照讀不存在了,MVCC降級爲Lock-Based CC。

結論:在MySQL/InnoDB中,所謂的讀不加鎖,並不適用於全部的狀況,而是和隔離級別有關。在Serializable隔離級別下,全部的操做都會加鎖。
一條簡單的刪除語句加鎖狀況也就分析完成了,可是學習不止於此,還在繼續,對於複雜SQL語句又是如何加鎖的呢?MySQL中的索引的分析又是怎樣的呢?性能分析、性能優化這些又是怎麼呢?還須要進一步的學習探索。


BLOG地址:www.liangsonghua.com

關注微信公衆號:松花皮蛋的黑板報,獲取更多精彩!

公衆號介紹:分享在京東工做的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

相關文章
相關標籤/搜索