前言
前言為本人寫這篇文章的牢騷,建議跳過不看。
之前好幾次都想好好的學習MySQL中的鎖,但是找了幾篇文章,看了一些鎖的型別有那麼多種,一時間也沒看懂是什麼意思,於是跟自己說先放鬆下自己,便從書桌起來在陽臺發呆、做做運動、打掃下公寓,時間就這樣過去了,真就印證了那句話:"當你在學習的時候,無論做什麼其他的事情都是有趣的。"但卻又能安慰自己:"我不是不想學習,只是在學習的過程中放鬆了下自己,下次再看吧吧",這樣心裡也不會存有罪惡感,安穩睡去,下次亦是重複如此。但是日誌一長,MySQL鎖就變成了一塊又硬又重的石頭,完全不想碰它。但是這幾天重新看了一遍,發現這塊石頭減肥了,於是便有了這篇文章。
MySQL的鎖
首先在閱讀文章之前,得先理解一個概念:鎖之間的互斥是作用於獲取鎖來說的。舉個例子,我拿到了一條記錄的X鎖,如果其他執行緒想要獲取這條記錄的其他鎖,那麼就需要等待,這就是互斥;但是如果其他執行緒不想獲取鎖,只是簡單的訪問,例如說select * from t where id = 1這些不用鎖的語句,並不需要獲取此記錄的鎖,那麼這個SQL不會進入阻塞,並不會形成互斥。
共享鎖/獨佔鎖(Shared and Exclusive Locks)
一種比較大的分類,不一定指行鎖,也可能是表鎖。如果是X鎖(獨佔鎖)則跟其他所有鎖形成互斥;如果是S鎖(共享鎖),則可實現讀讀(SS)並行。哈?你叫我舉個例子。okay,拿行鎖(記錄鎖)來說,當我們執行下面的SQL時當前事務拿到的是id為3的X行鎖。
update t set name = 'name1' where id = 3;
那麼代表什麼呢,代表如果上方的事務還沒結束的話,此時下面的操作都會被阻塞。
// 獲取記錄上的S鎖
select * from t where id = 3 lock in share mode;
// 獲取記錄上的X鎖
update t set name = 'name2' where id = 3;
也就是說,共享鎖和獨佔鎖的互斥關係如下表。
X | S | |
---|---|---|
X | 互斥 | 互斥 |
S | 互斥 | 相容 |
但是,這個時候我有一個問題。
如果執行的這個語句呢?
select * from t where id = 3;
是否還會出現阻塞的情況呢?好好的思考一下,然後想想自己為什麼要想這個沒有意義的問題。
答案是不會出現阻塞的情況,原因我在一開始的時候就說了,互斥是針對鎖的爭奪來說的,上面的這條SQL並不需要獲取到鎖,所以也就不會進入阻塞。
意向鎖(Intention Locks)
在講意向鎖之前,我們首先思考這樣的一個問題:
如果我想對整張表進行獨佔式鎖定(即X表鎖),我沒辦法像個莽夫一樣直接就鎖上了,我得知道表中的記錄有沒有被其他的事務鎖住,如果有的話那麼我就進入阻塞等待釋放,那要怎麼知道呢?
暴力點從第一條記錄開始遍歷到最後?不行,如果一張表有幾百萬甚至上千萬的話效率就會非常低下(別跟我說該分表了)。於是,為了解決這種效能問題,意向鎖應援而生。
意向鎖就是為了提升獲取表鎖前判斷而存在的鎖。首先意向鎖是表級別鎖(表鎖跟行鎖是可以同時存在不衝突),在獲取資料行上的獨佔鎖或者共享鎖之前首先得獲取到表級別的意向鎖,這樣子,當有其他執行緒想要進行鎖表的時候,看到已經存在意向鎖,那麼就進入阻塞狀態,而不用遍歷所有資料行判斷是否行上有鎖,這樣子就大幅提升了效能。
意向鎖也分共享意向鎖(IS)和獨佔意向鎖(IX)。意思也很簡單,獲取S鎖之前需要獲取的就是IS鎖,獲取X鎖之前需要獲取的就是IX鎖,其鎖之間的互斥關係如下圖。
X | S | |
---|---|---|
IX | 互斥 | 互斥 |
IS | 互斥 | 相容 |
看了上面的圖,可能有人會問:"兄弟,你這隻有跟獨佔/共享鎖的互斥圖,意向鎖之間的互斥關係呢?"
首先,我得跟你說,我絕對不是因為懶得畫圖才不畫的,因為意向鎖之間是不互斥的。你想嘛,本來意向鎖就是為了提升鎖表時的判斷效能而存在的,而且本身自己也是表級別的鎖,你還給整個互斥關係,到時候不給你堵得像北京三環一樣,所以不互斥是一級棒的。
記錄鎖(Record Locks)
在上方也簡單的提到過記錄鎖,記錄鎖也稱行鎖,是一種針對特定索引上的鎖,注意是索引,而且如果只有記錄鎖的話必須是唯一索引(意思就是說只有記錄鎖一種,而非臨建鎖),過程為通過唯一索引定位到對應的聚簇索引,然後將這條聚簇索引鎖住。如果唯一索引是多列組成的,但是查詢條件只用到了其中幾列,這種情況就不是施加記錄鎖,比方說,唯一索引為uniq_index(name,age)
,但是條件為where name = 'name1',這個時候用到的就不是記錄鎖而是臨建鎖了。
間隙鎖(Gap Locks)
對於一些非唯一索引的查詢或者範圍查詢的情況下,使用到的就是間隙鎖。怎麼理解呢,來看下官方給出的文件:
A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record.
意思就是說間隙鎖是在索引之間、第一個索引之前或者最後一個索引之後的鎖。這樣講可能還是有點抽象,舉個簡單的例子,假設現在有這樣的一張表:
id(bigint) | name(varchar) |
---|---|
3 | n3 |
5 | n5 |
7 | n7 |
再結合上面那句話的意思,那麼可以知道這張表中間隙鎖的取值範圍為
(負無窮,3),
(3,5),
(5,7),
(7, 正無窮)
有些文章的範圍會將右邊邊界也取上,但我覺得這樣反而不符合官方文件的定義,而且不好理解,不過取上也沒關係,因為間隙鎖一般不會單獨使用,一般使用的都是臨鍵鎖(臨鍵鎖的話會把記錄帶上,也就是邊界也取到了),所以鎖的範圍都是一樣的,看個人覺得哪個容易理解。另外關於間隙鎖的範圍,官方描述中有一段有趣的描述,是這樣的:
A gap might span a single index value, multiple index values, or even be empty.
意思就是說,間隙可以是一個索引、可以跨越多個索引,甚至可以是空的;換句話說,"根本就沒有間隙鎖,或者說,哪裡都是間隙鎖。"
另外需要注意:間隙鎖只有在RC級別及以上的隔離級別中才有,其他級別是沒有間隙鎖的。
講了這麼多可能還是雲裡霧裡的,來舉個簡單的例子,還是上面表格中的記錄。
id(bigint) | name(varchar) |
---|---|
3 | n3 |
5 | n5 |
7 | n7 |
id的值分別為3、5、7,那麼下面這條SQL的間隙範圍是多少呢?
select * from t where id between 3 and 5 for update;
你以為是(3,5)?其實是(3,5)和(5,7)噠,這也另一方面證實了官方說的間隙鎖可能跨越索引的說法。實際上由於臨鍵鎖的存在,會把記錄鎖也帶上,也就是邊界3、5、7都會被鎖住(即範圍為[3,7],已驗證,若有其他見解請在評論區狠狠的打我的臉,不要客氣,打得越狠越好)。
講到這裡,可能有人會說,”你說的這些我現在懂了,那這個間隙鎖到底有什麼用呢?"
emmm,先看下官方文件是怎麼說的:
Gap locks in
InnoDB
are “purely inhibitive”, which means that their only purpose is to prevent other transactions from inserting to the gap.
用我的工地英語翻譯過來就是,間隙鎖不會阻塞,唯一的作用就是防止其他事務往這個間隙中插入記錄;毫不客氣的說,間隙鎖除了防止在間隙之間插入記錄之外一無是處,間隙鎖之間甚至還可以共存(無論是X還是S間隙鎖),你鎖(3,5)跟我鎖(2,6)又有什麼關係呢?
臨鍵鎖(next-key locks)
上方介紹間隙鎖的時候多多少少也提到過,臨鍵鎖就是記錄鎖+間隙鎖的結合。一般來說使用的都是臨鍵鎖而非間隙鎖,上方的SQL:
select * from t where id between 3 and 5 for update;
鎖定的範圍帶上邊界也說明了,實際上邊界上的鎖都是記錄鎖,所以範圍才變成了[3, 7]。當然使用的是唯一索引的話會進化成記錄鎖(多列索引的情況需另外考慮,記錄鎖處已提到),下面這個SQL用的就是記錄鎖。
select * from t where id = 3 for update;
插入意向鎖
一種特殊的間隙鎖,在其間隙中可以進行非當前記錄的插入,目的是為了提高插入的併發,可以理解為間隙為1的間隙鎖。 同時也是一種意向鎖,只不過這是給插入專用的。
自增鎖
表鎖,一般用在設定了auto_increment
的欄位。大致就是先獲取鎖,然後將最大的ID自增,與其他鎖不同的時候,完成自增操作之後其就釋放了,不需要等待事務的完成。
本文為部落格園原創文章。
參考:
https://blog.csdn.net/qq_41026740/article/details/97408858
https://zhuanlan.zhihu.com/p/48269420
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-insert-intention-locks