你們在面試中有沒遇到面試官問你下面六句Sql的區別呢html
select * from table where id = ? select * from table where id < ? select * from table where id = ? lock in share mode select * from table where id < ? lock in share mode select * from table where id = ? for update select * from table where id < ? for update
若是你能清楚的說出,這六句sql在不一樣的事務隔離級別下,是否加鎖,加的是共享鎖仍是排他鎖,是否存在間隙鎖,那這篇文章就沒有看的意義了。
之因此寫這篇文章是由於目前爲止網上這方面的文章太片面,都只說了一半,且大多沒指明隔離級別,以及where
後跟的是否爲索引條件列。在此,我就不一一列舉那些有誤的文章了,你們能夠自行百度一下,大多都是講不清楚。
OK,要回答這個問題,先問本身三個問題mysql
OK,開始回答面試
本文假定讀者,看過個人《MySQL(Innodb)索引的原理》。若是沒看過,額,你記得三句話吧算法
下面囉嗦點基礎知識sql
共享鎖(S鎖):假設事務T1對數據A加上共享鎖,那麼事務T2能夠讀數據A,不能修改數據A。
排他鎖(X鎖):假設事務T1對數據A加上共享鎖,那麼事務T2不能讀數據A,不能修改數據A。
咱們經過update
、delete
等語句加上的鎖都是行級別的鎖。只有LOCK TABLE … READ
和LOCK TABLE … WRITE
才能申請表級別的鎖。
意向共享鎖(IS鎖):一個事務在獲取(任何一行/或者全表)S鎖以前,必定會先在所在的表上加IS鎖。
意向排他鎖(IX鎖):一個事務在獲取(任何一行/或者全表)X鎖以前,必定會先在所在的表上加IX鎖。數據庫
意向鎖存在的目的?優化
OK,這裏說一下意向鎖存在的目的。假設事務T1,用X鎖來鎖住了表上的幾條記錄,那麼此時表上存在IX鎖,即意向排他鎖。那麼此時事務T2要進行LOCK TABLE … WRITE
的表級別鎖的請求,能夠直接根據意向鎖是否存在而判斷是否有鎖衝突。翻譯
個人說法是來自官方文檔:
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
加上本身矯揉造做的看法得出。
ok,記得以下三種,本文就夠用了
Record Locks
:簡單翻譯爲行鎖吧。注意了,該鎖是對索引記錄進行加鎖!鎖是在加索引上而不是行上的。注意了,innodb必定存在聚簇索引,所以行鎖最終都會落到聚簇索引上!
Gap Locks
:簡單翻譯爲間隙鎖,是對索引的間隙加鎖,其目的只有一個,防止其餘事物插入數據。在Read Committed
隔離級別下,不會使用間隙鎖。這裏我對官網補充一下,隔離級別比Read Committed
低的狀況下,也不會使用間隙鎖,如隔離級別爲Read Uncommited
時,也不存在間隙鎖。當隔離級別爲Repeatable Read
和Serializable
時,就會存在間隙鎖。
Next-Key Locks
:這個理解爲Record Lock
+索引前面的Gap Lock
。記住了,鎖住的是索引前面的間隙!好比一個索引包含值,10,11,13和20。那麼,間隙鎖的範圍以下code
(negative infinity, 10] (10, 11] (11, 13] (13, 20] (20, positive infinity)
最後一點基礎知識了,你們堅持看完,這些是後面分析的基礎!
在mysql中select分爲快照讀和當前讀,執行下面的語句htm
select * from table where id = ?;
執行的是快照讀,讀的是數據庫記錄的快照版本,是不加鎖的。(這種說法在隔離級別爲Serializable
中不成立,後面我會補充。)
那麼,執行
select * from table where id = ? lock in share mode;
會對讀取記錄加S鎖 (共享鎖),執行
select * from table where id = ? for update
會對讀取記錄加X鎖 (排他鎖),那麼
加的是表鎖仍是行鎖呢?
針對這點,咱們先回憶一下事務的四個隔離級別,他們由弱到強以下所示:
Read Uncommited(RU)
:讀未提交,一個事務能夠讀到另外一個事務未提交的數據!Read Committed (RC)
:讀已提交,一個事務能夠讀到另外一個事務已提交的數據!Repeatable Read (RR)
:可重複讀,加入間隙鎖,必定程度上避免了幻讀的產生!注意了,只是必定程度上,並無徹底避免!我會在下一篇文章說明!另外就是記住從該級別纔開始加入間隙鎖(這句話記下來,後面有用到)!Serializable
:串行化,該級別下讀寫串行化,且全部的select
語句後都自動加上lock in share mode
,即便用了共享鎖。所以在該隔離級別下,使用的是當前讀,而不是快照讀。那麼關因而表鎖仍是行鎖,你們能夠看到網上最流傳的一個說法是這樣的,
InnoDB行鎖是經過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不一樣,後者是經過在數據塊中對相應數據行加鎖來實現的。 InnoDB這種行鎖實現特色意味着:只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖!
這句話你們能夠搜一下,都是你抄個人,我抄你的。那麼,這句話自己有兩處錯誤!
錯誤一:並非用表鎖來實現鎖表的操做,而是利用了Next-Key Locks
,也能夠理解爲是用了行鎖+間隙鎖來實現鎖表的操做!
爲了便於說明,我來個例子,假設有表數據以下,pId爲主鍵索引
pId(int) | name(varchar) | num(int) |
---|---|---|
1 | aaa | 100 |
2 | bbb | 200 |
7 | ccc | 200 |
執行語句(name列無索引)
select * from table where name = `aaa` for update
那麼此時在pId=1,2,7這三條記錄上存在行鎖(把行鎖住了)。另外,在(-∞,1)(1,2)(2,7)(7,+∞)上存在間隙鎖(把間隙鎖住了)。所以,給人一種整個表鎖住的錯覺!
ps:
對該結論有疑問的,可自行執行show engine innodb status;
語句進行分析。
錯誤二:全部文章都不提隔離級別!
注意我上面說的,之因此可以鎖表,是經過行鎖+間隙鎖來實現的。那麼,RU
和RC
都不存在間隙鎖,這種說法在RU
和RC
中還能成立麼?
所以,該說法只在RR
和Serializable
中是成立的。若是隔離級別爲RU
和RC
,不管條件列上是否有索引,都不會鎖表,只鎖行!
下面來對開始的問題做出解答,假設有表以下,pId爲主鍵索引
pId(int) | name(varchar) | num(int) |
---|---|---|
1 | aaa | 100 |
2 | bbb | 200 |
3 | bbb | 300 |
7 | ccc | 200 |
(1)select * from table where num = 200
不加任何鎖,是快照讀。
(2)select * from table where num > 200
不加任何鎖,是快照讀。
(3)select * from table where num = 200 lock in share mode
當num = 200,有兩條記錄。這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級S鎖,採用當前讀。
(4)select * from table where num > 200 lock in share mode
當num > 200,有一條記錄。這條記錄對應的pId=3,所以在pId=3的聚簇索引上加上行級S鎖,採用當前讀。
(5)select * from table where num = 200 for update
當num = 200,有兩條記錄。這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級X鎖,採用當前讀。
(6)select * from table where num > 200 for update
當num > 200,有一條記錄。這條記錄對應的pId=3,所以在pId=3的聚簇索引上加上行級X鎖,採用當前讀。
恩,你們應該知道pId是主鍵列,所以pId用的就是聚簇索引。此狀況其實和RC/RU+條件列非索引狀況是相似的。
(1)select * from table where pId = 2
不加任何鎖,是快照讀。
(2)select * from table where pId > 2
不加任何鎖,是快照讀。
(3)select * from table where pId = 2 lock in share mode
在pId=2的聚簇索引上,加S鎖,爲當前讀。
(4)select * from table where pId > 2 lock in share mode
在pId=3,7的聚簇索引上,加S鎖,爲當前讀。
(5)select * from table where pId = 2 for update
在pId=2的聚簇索引上,加X鎖,爲當前讀。
(6)select * from table where pId > 2 for update
在pId=3,7的聚簇索引上,加X鎖,爲當前讀。
這裏,你們可能有疑問
爲何條件列加不加索引,加鎖狀況是同樣的?
ok,實際上是不同的。在RC/RU隔離級別中,MySQL Server作了優化。在條件列沒有索引的狀況下,儘管經過聚簇索引來掃描全表,進行全表加鎖。可是,MySQL Server層會進行過濾並把不符合條件的鎖立即釋放掉,所以你看起來最終結果是同樣的。可是RC/RU+條件列非索引比本例多了一個釋放不符合條件的鎖的過程!
咱們在num列上建上非惟一索引。此時有一棵聚簇索引(主鍵索引,pId)造成的B+索引樹,其葉子節點爲硬盤上的真實數據。以及另外一棵非聚簇索引(非惟一索引,num)造成的B+索引樹,其葉子節點依然爲索引節點,保存了num列的字段值,和對應的聚簇索引。
這點能夠看看個人《MySQL(Innodb)索引的原理》。
接下來分析開始
(1)select * from table where num = 200
不加任何鎖,是快照讀。
(2)select * from table where num > 200
不加任何鎖,是快照讀。
(3)select * from table where num = 200 lock in share mode
當num = 200,因爲num列上有索引,所以先在 num = 200的兩條索引記錄上加行級S鎖。接着,去聚簇索引樹上查詢,這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級S鎖,採用當前讀。
(4)select * from table where num > 200 lock in share mode
當num > 200,因爲num列上有索引,所以先在符合條件的 num = 300的一條索引記錄上加行級S鎖。接着,去聚簇索引樹上查詢,這條記錄對應的pId=3,所以在pId=3的聚簇索引上加行級S鎖,採用當前讀。
(5)select * from table where num = 200 for update
當num = 200,因爲num列上有索引,所以先在 num = 200的兩條索引記錄上加行級X鎖。接着,去聚簇索引樹上查詢,這兩條記錄對應的pId=2,7,所以在pId=2,7的聚簇索引上加行級X鎖,採用當前讀。
(6)select * from table where num > 200 for update
當num > 200,因爲num列上有索引,所以先在符合條件的 num = 300的一條索引記錄上加行級X鎖。接着,去聚簇索引樹上查詢,這條記錄對應的pId=3,所以在pId=3的聚簇索引上加行級X鎖,採用當前讀。
RR級別須要多考慮的就是gap lock,他的加鎖特徵在於,不管你怎麼查都是鎖全表。以下所示
接下來分析開始
(1)select * from table where num = 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(2)select * from table where num > 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(3)select * from table where num = 200 lock in share mode
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(4)select * from table where num > 200 lock in share mode
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加S鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(5)select * from table where num = 200 for update
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加X鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
(6)select * from table where num > 200 for update
在pId = 1,2,3,7(全表全部記錄)的聚簇索引上加X鎖。而且在
聚簇索引的全部間隙(-∞,1)(1,2)(2,3)(3,7)(7,+∞)加gap lock
恩,你們應該知道pId是主鍵列,所以pId用的就是聚簇索引。該狀況的加鎖特徵在於,若是where
後的條件爲精確查詢(=
的狀況),那麼只存在record lock。若是where
後的條件爲範圍查詢(>
或<
的狀況),那麼存在的是record lock+gap lock。
(1)select * from table where pId = 2
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=2的聚簇索引上加S鎖,不存在gap lock。
(2)select * from table where pId > 2
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=3,7的聚簇索引上加S鎖。在(2,3)(3,7)(7,+∞)加上gap lock
(3)select * from table where pId = 2 lock in share mode
是當前讀,在pId=2的聚簇索引上加S鎖,不存在gap lock。
(4)select * from table where pId > 2 lock in share mode
是當前讀,在pId=3,7的聚簇索引上加S鎖。在(2,3)(3,7)(7,+∞)加上gap lock
(5)select * from table where pId = 2 for update
是當前讀,在pId=2的聚簇索引上加X鎖。
(6)select * from table where pId > 2 for update
在pId=3,7的聚簇索引上加X鎖。在(2,3)(3,7)(7,+∞)加上gap lock
(7)select * from table where pId = 6 [lock in share mode|for update]
注意了,pId=6是不存在的列,這種狀況會在(3,7)上加gap lock。
(8)select * from table where pId > 18 [lock in share mode|for update]
注意了,pId>18,查詢結果是空的。在這種狀況下,是在(7,+∞)上加gap lock。
這裏非聚簇索引,須要區分是否爲惟一索引。由於若是是非惟一索引,間隙鎖的加鎖方式是有區別的。
先說一下,惟一索引的狀況。若是是惟一索引,狀況和RR/Serializable+條件列是聚簇索引相似,惟一有區別的是:這個時候有兩棵索引樹,加鎖是加在對應的非聚簇索引樹和聚簇索引樹上!你們能夠自行推敲!
下面說一下,非聚簇索引是非惟一索引的狀況,他和惟一索引的區別就是經過索引進行精確查詢之後,不只存在record lock,還存在gap lock。而經過惟一索引進行精確查詢後,只存在record lock,不存在gap lock。老規矩在num列創建非惟一索引
(1)select * from table where num = 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=2,7的聚簇索引上加S鎖,在num=200的非彙集索引上加S鎖,在(100,200)(200,300)加上gap lock。
(2)select * from table where num > 200
在RR級別下,不加任何鎖,是快照讀。
在Serializable級別下,是當前讀,在pId=3的聚簇索引上加S鎖,在num=300的非彙集索引上加S鎖。在(200,300)(300,+∞)加上gap lock
(3)select * from table where num = 200 lock in share mode
是當前讀,在pId=2,7的聚簇索引上加S鎖,在num=200的非彙集索引上加S鎖,在(100,200)(200,300)加上gap lock。
(4)select * from table where num > 200 lock in share mode
是當前讀,在pId=3的聚簇索引上加S鎖,在num=300的非彙集索引上加S鎖。在(200,300)(300,+∞)加上gap lock。
(5)select * from table where num = 200 for update
是當前讀,在pId=2,7的聚簇索引上加S鎖,在num=200的非彙集索引上加X鎖,在(100,200)(200,300)加上gap lock。
(6)select * from table where num > 200 for update
是當前讀,在pId=3的聚簇索引上加S鎖,在num=300的非彙集索引上加X鎖。在(200,300)(300,+∞)加上gap lock
(7)select * from table where num = 250 [lock in share mode|for update]
注意了,num=250是不存在的列,這種狀況會在(200,300)上加gap lock。
(8)select * from table where num > 400 [lock in share mode|for update]
注意了,pId>400,查詢結果是空的。在這種狀況下,是在(400,+∞)上加gap lock。