史上最全MySQL加鎖分析

引言

大家在面試中有沒遇到面試官問你下面六句Sql的區別呢

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,要回答這個問題,先問自己三個問題

    • 當前事務隔離級別是什麼
    • id列是否存在索引
    • 如果存在索引是聚簇索引還是非聚簇索引呢?

    OK,開始回答

    (想自學習編程的小夥伴請搜索圈T社區,更多行業相關資訊更有行業相關免費視頻教程。完全免費哦!)

    正文

    • innodb一定存在聚簇索引,默認以主鍵作爲聚簇索引
    • 有幾個索引,就有幾棵B+樹(不考慮hash索引的情形)
    • 聚簇索引的葉子節點爲磁盤上的真實數據。非聚簇索引的葉子節點還是索引,指向聚簇索引B+樹。

    下面囉嗦點基礎知識

    鎖類型

    共享鎖(S鎖):假設事務T1對數據A加上共享鎖,那麼事務T2可以讀數據A,不能修改數據A。

    排他鎖(X鎖):假設事務T1對數據A加上共享鎖,那麼事務T2不能讀數據A,不能修改數據A。

    我們通過updatedelete等語句加上的鎖都是行級別的鎖。只有LOCK TABLE … READLOCK 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 ReadSerializable時,就會存在間隙鎖。

      Next-Key Locks:這個理解爲Record Lock+索引前面的Gap Lock。記住了,鎖住的是索引前面的間隙!比如一個索引包含值,10,11,13和20。那麼,間隙鎖的範圍如下

      (negative infinity, 10]
      (10, 11]
      (11, 13]
      (13, 20]
      (20, positive infinity)
      

        快照讀和當前讀

        最後一點基礎知識了,大家堅持看完,這些是後面分析的基礎!

        在mysql中select分爲快照讀和當前讀,執行下面的語句

        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

                RC/RU+條件列非索引

                (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鎖,採用當前讀。

                RC/RU+條件列是聚簇索引

                恩,大家應該知道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+條件列非索引比本例多了一個釋放不符合條件的鎖的過程!

                RC/RU+條件列是非聚簇索引

                我們在num列上建上非唯一索引。此時有一棵聚簇索引(主鍵索引,pId)形成的B+索引樹,其葉子節點爲硬盤上的真實數據。以及另一棵非聚簇索引(非唯一索引,num)形成的B+索引樹,其葉子節點依然爲索引節點,保存了num列的字段值,和對應的聚簇索引。

                接下來分析開始

                (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/Serializable+條件列非索引

                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

                RR/Serializable+條件列是聚簇索引

                恩,大家應該知道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+條件列是非聚簇索引

                    這裏非聚簇索引,需要區分是否爲唯一索引。因爲如果是非唯一索引,間隙鎖的加鎖方式是有區別的。

                    先說一下,唯一索引的情況。如果是唯一索引,情況和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。

                    發表評論
                    所有評論
                    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
                    相關文章