MySQL的鎖

Abstract

Mysql如何實現隔離級別 - 可重複讀和讀提交 源碼分析中介紹了MySQL中如何實現讀提交和可重複讀主要是通過版本鏈+ReadView組成的MVCC實現的. 但是其中還有一個很重要的點就是MySQL如何用可重複讀解決幻讀問題的. 本文將演示(1)MySQL的可重複讀中沒有幻讀, PostGreSQL有的. (2) MySQL中的鎖.

幻讀現象

什麼是幻讀

不同的隔離級別可能產生不同的"不穩定現象".
比如髒讀-- 讀到未提交的數據. 不可重複讀 – 對單條記錄的數據在同一事務中不能重複讀取. 幻讀 – 對於同一事務中的同一語句發現前後返回的記錄條數不一樣. 那麼多出來的或者消失的行在使用者看來就是"Phantom Records". 像影子一樣憑空出現了. 它一般發生在select … from xx where 或者 update from xxx where 這種.

MySQL怎麼解決的?

在MySQL的可重複讀隔離級別中解決了幻讀現象, 在開始試驗之前, 我們需要了解下MySQL中事務併發控制相關的第3個組件----鎖. 其他兩個分別是: 版本鏈 (存儲多個版本數據); ReadView (看到應該看到的數據); 而鎖則防止插入或者修改會產生錯誤的數據.

MySQL中的鎖

MySQL的鎖 大體上分爲行鎖和表鎖. 而行鎖又分爲如下幾種:

  • LOCK_REC_NOT_GAP : 單個行記錄上的鎖
  • LOCK_GAP: 間隙鎖, 鎖定一個範圍但是不包括記錄本身. 主要用來解決幻讀情況
  • LOCK_ORDINARY : 鎖定一個範圍並鎖定記錄本身. 就是這裏的next-key lock. 注意值代表的含義是(上一個indexrecord, 當前record]

MySQL演示

假設如下的表tt (a int primary key, b int); 然後在b上有非唯一索引.
數據如下:
在這裏插入圖片描述
如下同時開始兩個會話, 並且設置當前session隔離級別爲repeatable read進行試驗.
set session transaction isolation level repeatable read

Session1 Session2
begin;
select * from tt where b = 30 for update;
嘗試: insert into tt values (29, 29); – 失敗 block
insert into tt values (29, 31); --失敗block
insert into tt values (29, 18); --成功
insert into tt values (31, 35); – 成功
insert into tt values (29, 20); – 成功
insert into tt values (31, 34); --失敗

看出來了嗎? 前面2次插入都失敗了. 但是後面2次都成功了. 這裏跟b的值有關係. 注意到我們會話一的select好像以某種gap鎖的方式鎖住了b值爲(20, 34]的範圍. 注意集合開閉.

如何驗證??

在MySQL 8.0 中可以通過如下查詢查到鎖住的數據和類型:
SELECT * FROM performance_schema.data_locks\G;
輸出如下:

mysql> SELECT * FROM performance_schema.data_locks\G;
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:1064:140393245168136
ENGINE_TRANSACTION_ID: 19512
            THREAD_ID: 49
             EVENT_ID: 83
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140393245168136
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:7:5:5:140393236938776
ENGINE_TRANSACTION_ID: 19512
            THREAD_ID: 49
             EVENT_ID: 83
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: tt_b
OBJECT_INSTANCE_BEGIN: 140393236938776
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 30, 30
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:7:4:5:140393236939128
ENGINE_TRANSACTION_ID: 19512
            THREAD_ID: 49
             EVENT_ID: 83
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140393236939128
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 30
*************************** 4. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:7:5:7:140393236939480
ENGINE_TRANSACTION_ID: 19512
            THREAD_ID: 49
             EVENT_ID: 83
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: tt_b
OBJECT_INSTANCE_BEGIN: 140393236939480
            LOCK_TYPE: RECORD
            LOCK_MODE: X,GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 34, 44


第1行是table tt的IX 表鎖(for update導致的意向排它鎖).
第2行, 記錄索引tt_b上值爲30的排它鎖.
第3行,主鍵上的值爲30的排它鎖. – 爲啥會有2,3兩個鎖? 是因爲MySQL中的索引機制. 輔助索引和主鍵索引其實是分開存儲的. 這個暫時不說了.
第4行是一個GAP鎖, 鎖住了b=34, a=44的記錄.

關於GAP鎖

MySQL的文檔 說的還是比較複雜的. 比如上面的例子中取決於b是主鍵? 還是 有唯一索引? 有索引但是不是unique的? 或者沒有索引. 其實鎖住的記錄都不同.
如果是主鍵索引select * from tt where a = 30 for update輸出如下:

mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:1064:140393245168136
ENGINE_TRANSACTION_ID: 19520
            THREAD_ID: 49
             EVENT_ID: 87
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140393245168136
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:7:4:5:140393236938776
ENGINE_TRANSACTION_ID: 19520
            THREAD_ID: 49
             EVENT_ID: 87
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140393236938776
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 30

插入值爲(29, 29); (31, 29);的okay.
如果b是unique索引(與索引一致.)

mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:1064:140393245168136
ENGINE_TRANSACTION_ID: 19554
            THREAD_ID: 49
             EVENT_ID: 102
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140393245168136
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 140392970423376:7:4:5:140393236938776
ENGINE_TRANSACTION_ID: 19554
            THREAD_ID: 49
             EVENT_ID: 102
        OBJECT_SCHEMA: testdb
          OBJECT_NAME: tt
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140393236938776
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 30
2 rows in set (0.01 sec)

總結

簡單總結下: (1) 單純的select 不會加鎖. 但是select for update 和 update xx where 這種都會有對應的GAP鎖添加. (2) 爲啥select * from tt where b = 30 for update 會鎖住臨近的記錄呢? 我覺得這個可能是出於代碼一致性的考慮. 不論是select for update或者update where這種, 爲了防止幻讀現象產生, 所以周圍的記錄被鎖住了. 因爲b列上不是唯一索引, 那麼我們如何防止別人的update b=30呢? 直接防止別人修改完事.

發佈了78 篇原創文章 · 獲贊 30 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章