Record鎖/Gap鎖/Next-key鎖/插入意向鎖

文章總共分爲五個部分:

大而全版(五合一):InnoDB的鎖機制淺析(All in One)

前言

InnoDB常見的鎖有Record鎖、gap鎖、next-key鎖、插入意向鎖、自增鎖等。
下面會對每一種鎖給出一個查看鎖的示例。

常見的鎖有Record鎖、gap鎖、next-key鎖、插入意向鎖、自增鎖等。
下面會對每一種鎖給出一個查看鎖的示例。

1 準備工作

1.1 測試用表結構

示例的基礎是一個只有兩列的數據庫表。

mysql> CREATE TABLE test (
id int(11) NOT NULL,
code int(11) NOT NULL, 
PRIMARY KEY(id), 
KEY (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 

mysql> INSERT INTO test(id,code) values(1,1),(10,10);

數據表test只有兩列,id是主鍵索引,code是普通的索引(注意,一定不要是唯一索引),並初始化了兩條記錄,分別是(1,1),(10,10)。
這樣,我們驗證唯一鍵索引就可以使用id列,驗證普通索引(非唯一鍵二級索引)時就使用code列。

1.2 查看鎖狀態的方式

要看到鎖的情況,必須手動開啓多個事務,其中一些鎖的狀態的查看則必須使鎖處於waiting狀態,這樣才能在mysql的引擎狀態日誌中看到。

命令:

mysql> show engine innodb status;

這條命令能顯示最近幾個事務的狀態、查詢和寫入情況等信息。當出現死鎖時,命令能給出最近的死鎖明細。

2 記錄鎖 Record Locks

Record鎖

Record Lock是對索引記錄的鎖定。記錄鎖有兩種模式,S模式和X模式。
例如SELECT id FROM test WHERE id = 10 FOR UPDATE;表示防止任何其他事務插入、更新或者刪除id =10的行。

記錄鎖始終只鎖定索引。即使表沒有建立索引,InnoDB也會創建一個隱藏的聚簇索引(隱藏的遞增主鍵索引),並使用此索引進行記錄鎖定。

查看記錄鎖

開啓第一個事務,不提交,測試完之後回滾。

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update test set id=2 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

事務加鎖情況

mysql> show engine innodb status\G;
... 
------------
TRANSACTIONS
------------
---TRANSACTION 366811, ACTIVE 690 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 2
MySQL thread id 785, OS thread handle 123145432457216, query id 729076 localhost 127.0.0.1 root
...

可以看到有一行被加了鎖。由之前對鎖的描述可以推測出,update語句給id=1這一行上加了一個X鎖

注意:X鎖廣義上是一種抽象意義的排它鎖,即鎖一般分爲X模式S模式,狹義上指row或者index上的鎖,而Record鎖是索引上的鎖。
爲了不修改數據,可以用select ... for update語句,加鎖行爲和updatedelete是一樣的,insert加鎖機制較爲複雜,後面的章節會提到。

第一個事務保持原狀,不要提交或者回滾,現在開啓第二個事務。

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update test set id=3 where id=1;

執行update時,sql語句的執行被阻塞了。查看下事務狀態:

mysql> show engine innodb status\G;
...
------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 62 page no 3 n bits 72 index PRIMARY of table `test`.`test` trx id 366820 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
 0: len 8; hex 0000000000000001; asc         ;;
 1: len 6; hex 0000000598e3; asc       ;;
 2: len 7; hex 7e000001a80896; asc ~      ;;

------------------
...

喜聞樂見,我們看到了這個鎖的狀態。狀態標題是'事務正在等待獲取鎖',描述中的lock_mode X locks rec but not gap就是本章節中的record記錄鎖,直譯一下'X鎖模式鎖住了記錄'。後面還有一句but not gap意思是隻對record本身加鎖,並不對間隙加鎖,間隙鎖的敘述見下一個章節。

3 間隙鎖 Gap Locks

間隙鎖

間隙鎖作用在索引記錄之間的間隔,又或者作用在第一個索引之前,最後一個索引之後的間隙。不包括索引本身。
例如,SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;這條語句阻止其他事務插入10和20之間的數字,無論這個數字是否存在。

間隙可以跨越0個,單個或多個索引值。

間隙鎖是性能和併發權衡的產物,只存在於部分事務隔離級別。

select * from table where id=1;
唯一索引可以鎖定一行,所以不需要間隙鎖鎖定。
如果列沒有索引或者具有非唯一索引,該語句會鎖定當前索引前的間隙。

在同一個間隙上,不同的事務可以持有上述兼容/衝突表中衝突的兩個鎖。例如,事務T1現在持有一個間隙S鎖,T2可以同時在同一個間隙上持有間隙X鎖。
允許衝突的鎖在間隙上鎖定的原因是,如果從索引中清除一條記錄,則由不同事務在這條索引記錄上的加間隙鎖的動作必須被合併。

InnoDB中的間隙鎖的唯一目的是防止其他事務插入間隙。
間隙鎖是可以共存的,一個事務佔用的間隙鎖不會阻止另一個事務獲取同一個間隙上的間隙鎖。

如果事務隔離級別改爲RC,則間隙鎖會被禁用。

查看間隙鎖

按照官方文檔,where子句查詢條件是唯一鍵且指定了值時,只有record鎖,沒有gap鎖。
如果where語句指定了範圍,gap鎖是存在的。
這裏只測試驗證一下當指定非唯一鍵索引的時候,gap鎖的位置,按照文檔的說法,會鎖定當前索引及索引之前的間隙。(指定了非唯一鍵索引,例如code=10,間隙鎖仍然存在)

開啓第一個事務,鎖定一條非唯一的普通索引記錄

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where code = 10 for update;
+----+------+
| id | code |
+----+------+
| 10 |   10 |
+----+------+
1 row in set (0.00 sec)

由於預存了兩條數據,row(1,1)和row(10,10),此時這個間隙應該是1<gap<10。我們先插入row(2,2)來驗證下gap鎖的存在,再插入row(0,0)來驗證gap的邊界。

按照間隙鎖的官方文檔定義,select * from test where code = 10 for update;會鎖定code=10這個索引,並且會鎖定code<10的間隙。

開啓第二個事務,在code=10之前的間隙中插入一條數據,看下這條數據是否能夠插入。

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test values(2,2);

插入的時候,執行被阻塞,查看引擎狀態:

mysql> show engine innodb status\G;
...
---TRANSACTION 366864, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 793, OS thread handle 123145434963968, query id 730065 localhost 127.0.0.1 root update
insert into test values(2,2)
------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 63 page no 4 n bits 72 index code of table `test`.`test` trx id 366864 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 8; hex 800000000000000a; asc         ;;
 1: len 8; hex 000000000000000a; asc         ;;

------------------
...

插入語句被阻塞了,lock_mode X locks gap before rec,由於第一個事務鎖住了1到10之間的gap,需要等待獲取鎖之後才能插入。

如果再開啓一個事務,插入(0,0)

mysql> start transaction;
mysql> insert into test values(0,0);
Query OK, 1 row affected (0.00 sec)

可以看到:指定的非唯一建索引的gap鎖的邊界是當前索引到上一個索引之間的gap

最後給出鎖定區間的示例,首先插入一條記錄(5,5)

mysql> insert into test values(5,5);
Query OK, 1 row affected (0.00 sec)

開啓第一個事務:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where code between 1 and 10 for update;
+----+------+
| id | code |
+----+------+
|  1 |    1 |
|  5 |    5 |
| 10 |   10 |
+----+------+
3 rows in set (0.00 sec)

第二個事務,試圖去更新code=5的行:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update test set code=4 where code=5;

執行到這裏,如果第一個事務不提交或者回滾的話,第二個事務一直等待直至mysql中設定的超時時間。

4 Next-key Locks

Next-key鎖

Next-key鎖實際上是Record鎖和gap鎖的組合。Next-key鎖是在下一個索引記錄本身和索引之前的gap加上S鎖或是X鎖(如果是讀就加上S鎖,如果是寫就加X鎖)。
默認情況下,InnoDB的事務隔離級別爲RR,系統參數innodb_locks_unsafe_for_binlog的值爲false。InnoDB使用next-key鎖對索引進行掃描和搜索,這樣就讀取不到幻象行,避免了幻讀的發生。

幻讀是指在同一事務下,連續執行兩次同樣的SQL語句,第二次的SQL語句可能會返回之前不存在的行。

當查詢的索引是唯一索引時,Next-key lock會進行優化,降級爲Record Lock,此時Next-key lock僅僅作用在索引本身,而不會作用於gap和下一個索引上。

查看Next-key鎖

Next-key鎖的作用範圍

如上述例子,數據表test初始化了row(1,1),row(10,10),然後插入了row(5,5)。數據表如下:

mysql> select * from test;
+----+------+
| id | code |
+----+------+
|  1 |    1 |
|  5 |    5 |
| 10 |   10 |
+----+------+
3 rows in set (0.00 sec)

由於id是主鍵、唯一索引,mysql會做優化,因此使用code這個非唯一鍵的二級索引來舉例說明。

對於code,可能的next-key鎖的範圍是:

(-∞,1]
(1,5]
(5,10]
(10,+∞)

開啓第一個事務,在code=5的索引上請求更新:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test where code=5 for update;
+----+------+
| id | code |
+----+------+
|  5 |    5 |
+----+------+
1 row in set (8.81 sec)

之前在gap鎖的章節中介紹了,code=5 for update會在code=5的索引上加一個record鎖,還會在1<gap<5的間隙上加gap鎖。現在不再驗證,直接插入一條(8,8):

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test values(8);

insert處於等待執行的狀態,這就是next-key鎖生效而導致的結果。第一個事務,鎖定了區間(1,5],由於RR的隔離級別下next-key鎖處於開啓生效狀態,又鎖定了(5,10]區間。所以插入SQL語句的執行被阻塞。

解釋:在這種情況下,被鎖定的區域是code=5前一個索引到它的間隙,以及next-key的區域。code=5 for update對索引的鎖定用區間表示,gap鎖鎖定了(1,5),record鎖鎖定了{5}索引記錄,next-key鎖鎖住了(5,10],也就是說整個(1,10]的區間被鎖定了。由於是for update,所以這裏的鎖都是X鎖,因此阻止了其他事務中帶有衝突鎖定的操作執行。

如果我們在第一個事務中,執行了code>8 for update,在掃描過程中,找到了code=10,此時就會鎖住10之前的間隙(5到10之間的gap),10本身(record),和10之後的間隙(next-key)。此時另一個事務插入(6,6),(9,9)和(11,11)都是不被允許的,只有在前一個索引5及5之前的索引和間隙才能執行插入(更新和刪除也會被阻塞)。

5 插入意向鎖 Insert Intention Locks

插入意向鎖在行插入之前由INSERT設置一種間隙鎖,是意向排它鎖的一種。
在多事務同時寫入不同數據至同一索引間隙的時,不會發生鎖等待,事務之間互相不影響其他事務的完成,這和間隙鎖的定義是一致的。

假設一個記錄索引包含4和7,其他不同的事務分別插入5和6,此時只要行不衝突,插入意向鎖不會互相等待,可以直接獲取。參照鎖兼容/衝突矩陣。
插入意向鎖的例子不再列舉,可以查看gap鎖的第一個例子。

6 自增鎖

自增鎖(AUTO-INC Locks)是事務插入時自增列上特殊的表級別的鎖。最簡單的一種情況:如果一個事務正在向表中插入值,則任何其他事務必須等待,以便第一個事務插入的行接收連續的主鍵值。

我們一般把主鍵設置爲AUTO_INCREMENT的列,默認情況下這個字段的值爲0,InnoDB會在AUTO_INCREMENT修飾下的數據列所關聯的索引末尾設置獨佔鎖。在訪問自增計數器時,InnoDB使用自增鎖,但是鎖定僅僅持續到當前SQL語句的末尾,而不是整個事務的結束,畢竟自增鎖是表級別的鎖,如果長期鎖定會大大降低數據庫的性能。由於是表鎖,在使用期間,其他會話無法插入表中。

 

https://www.cnblogs.com/AaronCui/p/10508766.html

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