本文小结:介绍了Inodb加行锁的原则。
加锁规则总结
1、三种行锁的规则为:
(1)Next-Key是默认锁。左开右的闭理解:右闭是行锁(一行记录),左开-右开是间隙锁,所Next-KEY=行锁+间隙锁
(2)行锁场景
- 主键上&&等值查询,对主键B+树上元素。
- 唯一索引&&等值查询,对于主键B+树/唯一索引B+树上元素。
- 普通索引需要回表(等值&范围), 主键B+树上满足条件的元素都是行锁。
(2)间隙锁场景。也可以理解为出现幻读的场景
- 主键等值查询,只有在未找到元素时候,需要对向右第一个元素加间隙锁。
- 主键范围查询时,无论是否有结果,需要向右第一个元素加Next-Key。
- 二级索引等值查询时,无论是否命中结果,需要对向右第一个元素加间隙锁。
2、以select * from t where … for update为例加锁规则
(1)where等值查询
- 主键等值:select * from t where id =2 for update
- 找到
- 主键B+树, 加这个元素的行锁。
- 找不到
- 主键B+树, 加右边界的间隙锁
- 找到
- 唯一键等值:select * from t where uiq_key =2 for update
- 找到
- 二级索引uiq_key的B+树:加这个元素的行锁
- 主键B+树: 加这个元素的行锁
- 找不到
- 二级索引uiq_key的B+树:的右边界的 间隙锁。
- 找不到元素,就不会去回表,扫描主键元素,所以主键B+树不需要加任何锁
- 找到
- 普通索引等值:select * from t where idx_key =2 for update
- 找到
- 二级索引 idx_key的B+树:所有满足元素 都加NEXT-KEY;右边界第一个元素加间隙锁
- 主键B+树 :加行锁
- 找不到
- 二级索引idx_key 的B+树上:右边界第一个元素加间隙锁
- 找不到元素,就不会去回表,扫描主键元素,所以主键B+树不需要加任何锁
- 找到
(2)where 范围查询
- 主键:select * from t where id >2 for update
- 找到
- 主键B+树,满足条件的每个元素加Next-Key, 右边界第一个元素 加间隙锁。
- 找不到
- 主键B+树,向右第一个元素加Next-Key。
- 找到
- 唯一键:select * from t where uiq_key >2 for update
- 找到
- 二级索引uiq_key的B+树:满足条件的每个元素加Next-Key, 右边界第一个元素 加间隙锁。
- 主键B+:满足条件元素的行锁
- 找不到
- 二级索引uiq_key的B+树 向右第一个元素加Next-Key
- 找不到元素,就不会去回表,扫描主键元素,所以主键B+树不需要加任何锁
- 找到
- 普通索:select * from t where idx_key >2 for update
- 找到
- 二级索引idx_key的B+树:上所有满足元素&向右第一个元素 都加NEXT-KEY。
- 主键B+:满足条件元素的行锁
- 找不到
- 二级索引uiq_key的B+树 向右第一个元素加Next-Key
- 找不到元素,就不会去回表,扫描主键元素,所以主键B+树不需要加任何锁
- 找到
2、扫描元素
扫描元素包括满足条件和不满足条件的。以等值查询为例,主键和唯一索引找到元素之后,就不会再扫描,但是对于普通索引还需要继续扫描直到第一个不满足条件的元素。
准备数据
1、数据源
1 2 3 4 5 6 7 8 9 10 |
Create Table: CREATE TABLE `t` ( `id` int NOT NULL, `c` int NOT NULL, `d` int NOT NULL, `u` int DEFAULT NULL, PRIMARY KEY (`id`), // 主键 UNIQUE KEY `uiq_u` (`u`), // 唯一索引 KEY `idx_c` (`c`) // 普通索引 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec) |
数据
1 2 3 4 5 6 7 8 9 10 11 |
mysql> select * from t; +----+----+----+ | id | c | d | +----+----+----+ | 0 | 0 | 0 | | 5 | 5 | 5 | | 10 | 10 | 10 | | 15 | 15 | 15 | | 20 | 20 | 20 | | 25 | 25 | 25 | +----+----+----+ |
3、如何查看是哪种行锁
MySQL 8.0 开始information_schema.innodb_locks 改为 performance_schema.data_locks。
1 |
select * from performance_schema.data_locks \G; |
通过LOCK_MODE表示行锁、间隙锁、selectkey:
- 行锁:S,REC_NOT_GAP或X,REC_NOT_GAP。
- 间隙锁:S,GAP或X,GAP。
- Next-Key:S或X。
1 主键
1.1 等值
1、存在元素
(1)SQL举例
1 |
UPDATE `t` SET `d` = 4 WHERE id = 5 |
或者
1 |
select * from t where id = 5 for update; |
(2)加锁:
- 在5加上行锁(X,REC_NOT_GAP)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:10:140340086752288 ENGINE_TRANSACTION_ID: 100653 THREAD_ID: 3630 EVENT_ID: 128 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY // 索引名称-主键B+树 OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X,REC_NOT_GAP // 行锁 LOCK_STATUS: GRANTED LOCK_DATA: 5 // 元素 |
2、不存元素,则是在向右最后一个元素加selectkey
(1)SQL:
1 2 3 |
UPDATE `t` SET `d` = 4 WHERE id = 7 或者 select * from t where id = 7 for update; |
(2)加锁
- 在10是间隙锁(X,GAP)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:4:140340086752288 ENGINE_TRANSACTION_ID: 100611 THREAD_ID: 3630 EVENT_ID: 62 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X,GAP LOCK_STATUS: GRANTED LOCK_DATA: 10 |
1.2 范围查询
1、存在元素
(1)SQL:
1 |
UPDATE `t` SET `d` = 4 WHERE id >5 and id<20; |
(2)加锁:
主键在10、15加NEXT-KEY (X);20是间隙锁(X,GAP)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
*************************** 主键 *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:4:140340086752288 ENGINE_TRANSACTION_ID: 100612 THREAD_ID: 3630 EVENT_ID: 73 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 10 ***************************主键 *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:5:140340086752288 ENGINE_TRANSACTION_ID: 100612 THREAD_ID: 3630 EVENT_ID: 73 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 15 ***************************主键 *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:6:140340086752632 ENGINE_TRANSACTION_ID: 100612 THREAD_ID: 3630 EVENT_ID: 73 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752632 LOCK_TYPE: RECORD LOCK_MODE: X,GAP LOCK_STATUS: GRANTED LOCK_DATA: 20 |
2 普通索引
通过普通索引需要回表查询主键时,主键B+树上满足条件的元素都是行锁
2.1 等值
1、存在元素
(1) SQL
1 |
UPDATE `t` SET `d` = 4 WHERE c =5; |
(2)加锁
- 二级索引,5 加 NEXT-KEY( X ),c=10 加间隙锁(X,GAP)。c=10 为向右最后一个元素
- 主键上, c=5加行锁(X,REC_NOT_GAP)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
*************************** 二级索引 *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:5:3:140340086752288 ENGINE_TRANSACTION_ID: 100614 THREAD_ID: 3630 EVENT_ID: 77 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: idx_c OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 5, 5 *************************** 主键 *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:3:140340086752632 ENGINE_TRANSACTION_ID: 100614 THREAD_ID: 3630 EVENT_ID: 77 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752632 LOCK_TYPE: RECORD LOCK_MODE: X,REC_NOT_GAP LOCK_STATUS: GRANTED LOCK_DATA: 5 *************************** 二级索引 *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:5:4:140340086752976 ENGINE_TRANSACTION_ID: 100614 THREAD_ID: 3630 EVENT_ID: 77 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: idx_c OBJECT_INSTANCE_BEGIN: 140340086752976 LOCK_TYPE: RECORD LOCK_MODE: X,GAP LOCK_STATUS: GRANTED LOCK_DATA: 10, 10 |
2、不满足元素
(1)SQL
UPDATE t
SET d
= 4 WHERE c =7;
(2)加锁:
- 二级索引:c=10,加间隙锁(X,GAP)。 10为向右最后一个元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:5:4:140340086752288 ENGINE_TRANSACTION_ID: 100616 THREAD_ID: 3630 EVENT_ID: 81 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: idx_c OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X,GAP LOCK_STATUS: GRANTED LOCK_DATA: 10, 10 |
2.2 范围查询
1、存在元素
(1)SQL:
1 |
UPDATE `t` SET `d` = 4 WHERE c >5 and c<15; |
(2)加锁:
- 二级索引B+树:10加 NEXT-KYE(X )。 15(右边界)NEXT-KYE(X )
- 主键B+树:10 加行锁 ( X,REC_NOT_GAP)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
*************************** 2. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:5:4:140340086752288 ENGINE_TRANSACTION_ID: 100662 THREAD_ID: 3630 EVENT_ID: 159 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: idx_c OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 10, 10 *************************** 3. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:5:5:140340086752288 ENGINE_TRANSACTION_ID: 100662 THREAD_ID: 3630 EVENT_ID: 159 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: idx_c OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 15, 15 *************************** 4. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:11:140340086752632 ENGINE_TRANSACTION_ID: 100662 THREAD_ID: 3630 EVENT_ID: 159 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752632 LOCK_TYPE: RECORD LOCK_MODE: X,REC_NOT_GAP LOCK_STATUS: GRANTED LOCK_DATA: 10 |
3 全表扫描
没有命中任何索引,全表扫描主键B+树。
1、存在元素
(1)SQL:
select * from t WHERE d =5 for update;
(2)加锁:
所有元素都加了NEXT-KEY(X) ,还有一个supremum pseudo-record也加了NEXT-KEY
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
*************************** 2. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:1:140340086752288 ENGINE_TRANSACTION_ID: 100629 THREAD_ID: 3630 EVENT_ID: 115 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: supremum pseudo-record *************************** 3. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:2:140340086752288 ENGINE_TRANSACTION_ID: 100629 THREAD_ID: 3630 EVENT_ID: 115 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 0 *************************** 4. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:3:140340086752288 ENGINE_TRANSACTION_ID: 100629 THREAD_ID: 3630 EVENT_ID: 115 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 5 *************************** 5. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:4:140340086752288 ENGINE_TRANSACTION_ID: 100629 THREAD_ID: 3630 EVENT_ID: 115 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 10 *************************** 6. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:5:140340086752288 ENGINE_TRANSACTION_ID: 100629 THREAD_ID: 3630 EVENT_ID: 115 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 15 *************************** 7. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:6:140340086752288 ENGINE_TRANSACTION_ID: 100629 THREAD_ID: 3630 EVENT_ID: 115 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 20 *************************** 8. row *************************** ENGINE: INNODB ENGINE_LOCK_ID: 140338736763864:60:4:7:140340086752288 ENGINE_TRANSACTION_ID: 100629 THREAD_ID: 3630 EVENT_ID: 115 OBJECT_SCHEMA: test OBJECT_NAME: t PARTITION_NAME: NULL SUBPARTITION_NAME: NULL INDEX_NAME: PRIMARY OBJECT_INSTANCE_BEGIN: 140340086752288 LOCK_TYPE: RECORD LOCK_MODE: X LOCK_STATUS: GRANTED LOCK_DATA: 25 |
参考
1、mysql45讲 :https://time.geekbang.org/column/article/75659