記錄鎖(Record LockRecord)、間隙鎖(Gap Lock)與 Next-Key Lock
1. Record LockRecord
Lock 也就是我們所說的記錄鎖,記錄鎖是對索引記錄的鎖,注意,它是針對索引記錄,即它只鎖定記錄這一行資料
1.將系統變數 innodb_status_output_locks 設定為 預設為OFF
mysql> show variables like 'innodb_status_output_locks';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_status_output_locks | OFF |
+----------------------------+-------+
1 row in set (0.00 sec),
mysql> set global innodb_status_output_locks=ON;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'innodb_status_output_locks';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_status_output_locks | ON |
+----------------------------+-------+
1 row in set (0.00 sec)
2.執行如下 SQL,鎖定一行資料,此時會自動為表加上 IX 鎖:
mysql> begin; #開始事務
Query OK, 0 rows affected (0.00 sec)
mysql> select * from city where id=1 for update;
+----+-------+-------------+----------+------------+
| ID | Name | CountryCode | District | Population |
+----+-------+-------------+----------+------------+
| 1 | Kabul | AFG | Kabol | 1000 |
+----+-------+-------------+----------+------------+
1 row in set (0.00 sec)
mysql> show engine innodb status\G; #檢視 InnoDB 儲存引擎的情況
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2022-05-07 19:07:18 139841320953600 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 9 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 1 srv_active, 0 srv_shutdown, 1284 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 1
OS WAIT ARRAY INFO: signal count 1
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS #主要關注這裡的TRANSACTIONS
------------
Trx id counter 18190
Purge done for trx's n:o < 18183 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421316317542832, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 421316317542024, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 18189, ACTIVE 4 sec
2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 8, OS thread handle 139841320953600, query id 25 localhost root starting
show engine innodb status
TABLE LOCK table `world`.`city` trx id 18189 lock mode IX **************
RECORD LOCKS space id 5 page no 6 n bits 248 index PRIMARY of table `world`.`city` trx id 18189 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 7; compact format; info bits 0 ********
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000000430e; asc C ;;
2: len 7; hex 02000001140151; asc Q;;
3: len 30; hex 4b6162756c20202020202020202020202020202020202020202020202020; asc Kabul ; (total 35 bytes);
4: len 3; hex 414647; asc AFG;;
5: len 20; hex 4b61626f6c202020202020202020202020202020; asc Kabol ;;
6: len 4; hex 800003e8; asc ;;
--------
TABLE LOCK table `world`.`city` trx id 18189 lock mode IX
這句就是說事務 id 為 18189 的事務,為 city 表添加了意向排他鎖(IX)。
RECORD LOCKS space id 5 page no 6 n bits 248 index PRIMARY of table `world`.`city` trx id 18189 lock_mode X locks rec but not gap
Record lock, 這句就是一個鎖結構的記錄,這裡的索引是 PRIMARY,加的鎖也是正兒八經的記錄鎖(not gap)。
LOCKS REC BUT NOT GAP,有為句就說明這是一個記錄鎖
Record Lock 和我們之前所講的 S 鎖以及 X 鎖有什麼區別呢?S 鎖是共享鎖,X 鎖是排他鎖,當我們加 S 鎖或者 X 鎖的時候,如果用到了索引,鎖加在了某一條具體的記錄上,那麼這個鎖也是一個記錄鎖(其實,記錄鎖,S 鎖,X 鎖,概念有一些重複的地方,但是描述的重點不一樣)。
2. Gap Lock
Gap Lock 也叫做間隙鎖,它的存在可以解決幻讀問題,另外需要注意,Gap Lock 也只在 REPEATABLE READ 隔離級別下有效。先來看看什麼是幻讀,我們來看如下一個表格:
有兩個會話,A 和 B,先在會話 A 中開啟事務,然後查詢 age 為 99 的使用者總數,注意使用當前讀,因為在預設的隔離級別下,預設的快照讀並不能讀到其他事務提交的資料,至於快照讀和當前讀的區別。當會話 A 中第一次查詢過後,會話 B 中向資料庫添加了一行記錄,等到會話 A 中第二次查詢的時候,就查到了和第一次查詢不一樣的結果,這就是幻讀(注意幻讀專指資料插入引起的不一致)。
在 MySQL 8.0預設的隔離級別 REPEATABLE READ 下,上圖所描述的情況無法復現。無法復現的原因在於,在 MySQL 的 REPEATABLE READ 隔離級別中,它已經幫我們解決了幻讀問題,解決的方案就是 Gap Lock。
之所以出現幻讀的問題,是因為記錄之間存在縫隙,使用者可以往這些縫隙中插入資料,這就導致了幻讀問題,如下圖:
id 之間有縫隙,有縫隙就有漏洞。前面我們所說的記錄鎖只能鎖住一條具體的記錄,但是對於記錄之間的空隙卻無能無力,這就導致了幻讀(其他事務可往縫隙中插入資料)。現在 Gap Lock 間隙鎖,就是要把這些記錄之間的間隙也給鎖住,間隙鎖住了,就不用擔心幻讀問題了,這也是 Gap Lock 存在的意義。
給一條記錄加 Gap Lock,是鎖住了這條記錄前面的空隙,例如給 id 為 1 的記錄加 Gap Lock,鎖住的範圍是 (-∞,1),給 id 為 3 的記錄加 Gap Lock,鎖住的範圍是 (1,3),那麼 id 為 10 後面的空隙怎麼鎖定呢?MySQL 提供了一個 Supremum 表示當前頁面中的最大記錄,所以最後針對 Supremum 鎖住的範圍就是 (10,+∞),這樣,所有的間隙都被覆蓋到了,由於鎖定的是間隙,所以都是開區間。
簡單的例子:
1.建立一個表:
mysql> CREATE TABLE `T4` (
-> `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
-> `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
-> `age` int(11) DEFAULT NULL,
-> PRIMARY KEY (`id`),
-> KEY `age` (`age`)
-> ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Query OK, 0 rows affected, 2 warnings (0.03 sec)
插入以下資料:
mysql> insert into T4 values('1','xiaoli',99);
Query OK, 1 row affected (0.00 sec)
mysql> insert into T4 values('2','xiaoming',89);
Query OK, 1 row affected (0.00 sec)
mysql> insert into T4 values('3','xiaoliu',79);
Query OK, 1 row affected (0.00 sec)
mysql> select * from T4;
+----+----------+------+
| id | username | age |
+----+----------+------+
| 1 | xiaoli | 99 |
| 2 | xiaoming | 89 |
| 3 | xiaoliu | 79 |
+----+----------+------+
3 rows in set (0.00 sec)
2.執行如下 SQL,鎖定一行資料,此時也會產生間隙鎖
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from T4 force index(age) where age=89 for update;
+----+----------+------+
| id | username | age |
+----+----------+------+
| 2 | xiaoming | 89 |
+----+----------+------+
1 row in set (0.00 sec)
3.檢視innodb引擎狀況
mysql> show engine innodb status\G;
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2022-05-07 19:56:28 139841320953600 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 22 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 6 srv_active, 0 srv_shutdown, 4210 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 3
OS WAIT ARRAY INFO: signal count 3
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 18208
Purge done for trx's n:o < 18205 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421316317542832, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 421316317542024, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 18207, ACTIVE 58 sec
4 lock struct(s), heap size 1128, 3 row lock(s)
MySQL thread id 8, OS thread handle 139841320953600, query id 37 localhost root starting
show engine innodb status
TABLE LOCK table `world`.`T4` trx id 18207 lock mode IX
RECORD LOCKS space id 8 page no 5 n bits 72 index age of table `world`.`T4` trx id 18207 lock_mode X ****記錄鎖
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000059; asc Y;;
1: len 4; hex 00000002; asc ;;
RECORD LOCKS space id 8 page no 4 n bits 72 index PRIMARY of table `world`.`T4` trx id 18207 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 *****間隙鎖
一個間隙鎖的加鎖記錄,可以看到,在某一個記錄之前加了間隙鎖。這就是間隙鎖。
非常重要注意:Gap Lock 只在 REPEATABLE READ 隔離級別下有效。
3. Next-Key Lock
既想鎖定一行,又想鎖定行之間的記錄,那麼就是 Next-Key Lock 了,換言之,Next-Key Lock 是 Record Lock 和 Gap Lock 的結合體。
Next-Key Lock 的加鎖規則: 鎖的範圍是左開右閉。 如果是唯一非空索引的等值查詢,Next-Key Lock 會退化成 Record Lock。 普通索引上的等值查詢,向後遍歷時,最後一個不滿足等值條件的時候,Next-Key Lock 會退化成 Gap Lock。
舉例:
建立一個學生表:
mysql> CREATE TABLE `student` (
-> `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
-> `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
-> `score` double NOT NULL,
-> PRIMARY KEY (`id`),
-> UNIQUE KEY `score` (`score`)
-> ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Query OK, 0 rows affected, 1 warning (0.02 sec)
插入資料:
mysql> insert into student values ('1','a','90');
Query OK, 1 row affected (0.00 sec)
mysql> insert into student values ('2','b','89');
Query OK, 1 row affected (0.00 sec)
mysql> insert into student values ('3','c','95');
Query OK, 1 row affected (0.01 sec)
mysql> insert into student values ('4','d','80');
Query OK, 1 row affected (0.00 sec)
mysql> insert into student values ('5','e','79');
Query OK, 1 row affected (0.01 sec)
檢視資料:
mysql> select * from student;
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1 | a | 90 |
| 2 | b | 89 |
| 3 | c | 95 |
| 4 | d | 80 |
| 5 | e | 79 |
+----+------+-------+
5 rows in set (0.00 sec)
執行以下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student force index(score) where score=90 for update;
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1 | a | 90 |
+----+------+-------+
1 row in set (0.00 sec)
檢視引擎狀況:
mysql> show engine innodb status\G;
*************************** 1. row ***************************
Type: InnoDB
Name:
Status:
=====================================
2022-05-07 20:14:25 139841320953600 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 40 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 12 srv_active, 0 srv_shutdown, 5274 srv_idle
srv_master_thread log flush and writes: 0
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 9
OS WAIT ARRAY INFO: signal count 9
RW-shared spins 0, rounds 0, OS waits 0
RW-excl spins 0, rounds 0, OS waits 0
RW-sx spins 0, rounds 0, OS waits 0
Spin rounds per wait: 0.00 RW-shared, 0.00 RW-excl, 0.00 RW-sx
------------
TRANSACTIONS
------------
Trx id counter 18231
Purge done for trx's n:o < 18229 undo n:o < 0 state: running but idle
History list length 0
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421316317542832, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 421316317542024, not started
0 lock struct(s), heap size 1128, 0 row lock(s)
---TRANSACTION 18230, ACTIVE 51 sec
3 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 8, OS thread handle 139841320953600, query id 49 localhost root starting
show engine innodb status
TABLE LOCK table `world`.`student` trx id 18230 lock mode IX ****************
RECORD LOCKS space id 9 page no 5 n bits 72 index score of table `world`.`student` trx id 18230 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0 **************************
由於 score 是唯一非空索引,所以 Next-Key Lock 會退化成 Record Lock,換句話說,這行 SQL 只給 score 為 90 的記錄加鎖,不存在 Gap Lock,即我們新開一個會話,插入一條 score 為 88 的記錄也是 OK 的。
mysql> insert into student values ('6','f','88');
Query OK, 1 row affected (0.00 sec)
由於並不存在 score 為 91 的記錄,所以這裡會產生一個範圍為 (90,95) 的間隙鎖,我們執行如下 SQL 可以驗證:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student force index(score) where score=91 for update;
Empty set (0.00 sec)
另外一個會話中插入一個90.1的score,出現間隙鎖(90~95),90~95資料都新增不進去。
mysql> insert into student values ('7','g','90.1');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into student values ('8','h','90');
ERROR 1062 (23000): Duplicate entry '90' for key 'student.score'
mysql> insert into student values ('9','j','94.9');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into student values ('10','l','95.1'); ********插入95以上資料可以
Query OK, 1 row affected (0.00 sec)
總結:
可以看到,90.1、94.9 都會被阻塞(我按了 Ctrl C,所以大家看到查詢終止)。
90、95 則不符合唯一非空索引的條件。
95.1 則可以插入成功
參考文獻:
https://www.51cto.com/article/707803.html