结论
简介
记录锁:锁定的是某一行一级;
间隙锁: 锁定的是记录与记录之间的空隙,间隙锁只阻塞插入操作;
临键锁: 是记录锁、间隙锁(与前一条数据的间隙)的并集,是mysql加锁的基本单位;
mysql会根据不同索引的不同查询情况,进行锁的切换
临键锁为了解决「可重复读」下「当前读」中出现的幻读问题;
如何防止幻读:
- 防止间隙内有新数据被插入。(间隙锁)
- 防止已存在的数据,更新成间隙内的数据。(记录锁)
1、环境准备
1.1 数据脚本
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`age` int DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=67 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (1, '小a', 10);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (7, '小b', 10);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (8, '小c', 10);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (9, '小d', 20);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (10, '小e', 20);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (11, '小f', 20);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (12, '小g', 30);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (15, '小h', 30);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (20, '小i', 30);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (56, '小j', 40);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (58, '小k', 40);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (65, '小l', 50);
INSERT INTO `test`.`user`(`id`, `username`, `age`) VALUES (66, '小m', 60);
1.2 表数据
mysql> select * from user;
+----+----------+------+
| id | username | age |
+----+----------+------+
| 1 | 小a | 10 |
| 7 | 小b | 10 |
| 8 | 小c | 10 |
| 9 | 小d | 20 |
| 10 | 小e | 20 |
| 11 | 小f | 20 |
| 12 | 小g | 30 |
| 15 | 小h | 30 |
| 20 | 小i | 30 |
| 56 | 小j | 40 |
| 58 | 小k | 40 |
| 65 | 小l | 50 |
| 66 | 小m | 60 |
+----+----------+------+
2、扩展语句
2.1、 查询当前事物、锁语句
/*查看正在运行的事务*/
select * from information_schema.innodb_trx;
/*查看当前的锁信息*/
select * from `performance_schema`.data_locks;
/*查看锁等待的信息*/
select * from `performance_schema`.data_lock_waits
2.2、data_locks关键字段
字段名 | 字段含义 |
---|---|
ENGINE_TRANSACTION_ID | 「innodb_trx」表中的事物id(trx_id) |
OBJECT_SCHEMA | 数据库名 |
OBJECT_NAME | 表名 |
INDEX_NAME | 索引名 |
LOCK_TYPE | 锁类型(TABLE/RECORD) |
LOCK_MODE | 锁模式: IX: 表意向排它锁; X: NextKey-Lock 临键锁; X, REC_NOT_GAP: 记录锁; X,GAP: 间隙锁; LOCK_INSERT_INTENTION:插入意向锁; |
LOCK_DATA | 锁的数据,当LOCK_TYPE为RECORD时才会有值(如果是聚族索引则直接显示主键,如果是非聚族索引则是,当前数据以及主键数据) |
3、主键索引
3.1、 等值查询
case1:等值查询,可以查到主键记录;
# 等值查询
select * from user where id = 10 for update;
# 查询当前锁信息
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schem
a`.data_locks;
+------------+-----------+---------------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+---------------+-----------+
| NULL | TABLE | IX | NULL |
| PRIMARY | RECORD | X,REC_NOT_GAP | 10 |
+------------+-----------+---------------+-----------+
分析:
-
存在的锁。
- 第1把锁:表锁,目的是如果有对表加锁需求的话,不需要循环判断每行的锁数据;
- 第2把锁:行锁,主键索引,id=10;
-
临键锁变为记录锁;id为10的记录被锁定,不可删除、编辑;
case2:等值查询,查不到主键记录;
# 等值查询不存在的数据
select * from user where id = 5 for update;
# 查询当前锁信息
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+-----------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+-----------+-----------+
| NULL | TABLE | IX | NULL |
| PRIMARY | RECORD | X,GAP | 7 |
+------------+-----------+-----------+-----------+
分析:
-
存在的锁
- 第1把锁:表锁(可忽略);
- 第2把锁:行锁,主键索引,间隙锁,id=7,加锁范围:(1,7);
-
加锁范围:间隙锁(1,7),以下这些数据:2、3、4、5、6不可进行插入;
3.2、范围查询
case1:范围数据都可以查询到
# 范围查询,数据都存在
select * from user where id BETWEEN 10 and 12 for update;
# 查询当前锁信息
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+---------------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+---------------+-----------+
| NULL | TABLE | IX | NULL |
| PRIMARY | RECORD | X,REC_NOT_GAP | 10 |
| PRIMARY | RECORD | X | 11 |
| PRIMARY | RECORD | X | 12 |
+------------+-----------+---------------+-----------+
分析:
-
存在的锁
- 第1把锁:表锁(忽略)
- 第2把锁:主键索引,记录锁,id=10;
- 第3把锁:主键索引,临键锁,id=11,加锁范围: (10,11],退化为记录锁 ;
- 第4把锁:主键索引,临键锁,id=12,加锁范围: (11,12],退化为记录锁 ;
-
id: 10、11、12这三条记录无法进行update、delete,其它数据不受影响。
case2:范围数据部分可以查询到
# 范围查询,部分存在,部分不存在
select * from user where id BETWEEN 13 and 25 for update;
# 查询当前锁信息
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+-----------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+-----------+-----------+
| NULL | TABLE | IX | NULL |
| PRIMARY | RECORD | X | 15 |
| PRIMARY | RECORD | X | 20 |
| PRIMARY | RECORD | X,GAP | 56 |
+------------+-----------+-----------+-----------+
分析:
-
存在的锁
- 第1把锁:表锁(忽略);
- 第2把锁:主键索引,临键锁,id=15,加锁范围:(12,15];
- 第3把锁:主键索引,临键锁,id=20,加锁范围:(15,20];
- 第4把锁:主键索引,间隙锁,id=56,加锁范围:(20,56);
-
通过上述锁可以得出:
- 行锁:id=15、id=20,这两条数据不可编辑、删除;
- 间隙锁:(15,56),这个区间内不可新增数据;
case3:范围数据部分都查询不到
# 范围内数据都不存在
select * from user where id BETWEEN 30 and 40 for update;
# 查询当前锁信息
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+-----------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+-----------+-----------+
| NULL | TABLE | IX | NULL |
| PRIMARY | RECORD | X,GAP | 56 |
+------------+-----------+-----------+-----------+
分析:
-
存在的锁;
- 第1把锁:表锁(忽略);
- 第2把锁:主键索引,间隙锁,id=56,加锁范围:(20,56);
-
通过上述可以得出:(20,56)之间不可以插入数据,其它数据不受影响;
4、唯一索引
唯一索引与主键索引基本一致,只不过对唯一索引加锁的时候也会为主键索引加一份;
5、普通索引
5.1、等值查询
规则一:非唯一索引的等值查询,如果索引数据存在,除了临键锁以外会额外再加间隙锁,规则是右边第一个不符合条件的索引;(以防再向索引右侧插入数据);
规则二:非唯一索引的等值查询,如果索引数据不存在,临键锁变为间隙锁, 不会再向后走索引了;
case1:可以查询到数据
# 查询age=20的数据
select * from user where age = 20 for update;
# 查询当前锁信息
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+---------------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+---------------+-----------+
| NULL | TABLE | IX | NULL |
| idx_age | RECORD | X | 20, 9 |
| idx_age | RECORD | X | 20, 10 |
| idx_age | RECORD | X | 20, 11 |
| PRIMARY | RECORD | X,REC_NOT_GAP | 9 |
| PRIMARY | RECORD | X,REC_NOT_GAP | 10 |
| PRIMARY | RECORD | X,REC_NOT_GAP | 11 |
| idx_age | RECORD | X,GAP | 30, 12 |
+------------+-----------+---------------+-----------+
分析:
-
存在的锁
- 第1把锁:表锁(忽略);
- 第2、3、4把锁:行锁,age索引,临键锁,age=20,加锁范围:(10,20];
- 第5、6、7把锁:行锁,主键索引,记录锁,加锁范围:id=9、10、11;
- 第8把锁:行锁,age索引,间隙锁,age=30,加锁范围: (20,30) 【规则一】;
-
综上可得出:
-
间隙锁范围: (10,30),记录锁范围:id=9、10、11;
-
无法更新、删除 id= 9,10,11 或 age = 20 的数据;
-
无法插入age=(10,30)的数据。但不包含10,30,如果插入10、30,需要注意id上下区间的限制。
比如:
插入id=6 age=10,可以
插入id=13 age=10,不可以
插入id=21 age=30,可以
插入id=5 age=30,不可以 -
case2:未查询到数据
# 查询age=25的信息
select * from user where age = 25 for update;
# 查询当前锁信息
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+-----------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+-----------+-----------+
| NULL | TABLE | IX | NULL |
| idx_age | RECORD | X,GAP | 30, 12 |
+------------+-----------+-----------+-----------+
分析:
-
存在的锁;
- 第1把锁:表锁(忽略);
- 第2把锁:行锁,间隙锁,age=30,加锁范围: (20,30)
-
综上可得出
- 间隙锁范围: (20,30);
- 无法插入age=(20,30)的数据,但有id上下区间的限制,可能会无法插入;
5.2、范围查询
规则三:非唯一索引的等值查询,加临键锁;
case1:可以查询到数据
# age范围查询
select * from user where age between 18 and 28 for update;
# 查询2
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+---------------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+---------------+-----------+
| NULL | TABLE | IX | NULL |
| idx_age | RECORD | X | 20, 9 |
| idx_age | RECORD | X | 20, 10 |
| idx_age | RECORD | X | 20, 11 |
| idx_age | RECORD | X | 30, 12 |
| PRIMARY | RECORD | X,REC_NOT_GAP | 9 |
| PRIMARY | RECORD | X,REC_NOT_GAP | 10 |
| PRIMARY | RECORD | X,REC_NOT_GAP | 11 |
+------------+-----------+---------------+-----------+
分析:
-
存在的锁
- 第1把锁:表锁;
- 第2、3、4把锁:行锁,age索引,临键锁,age=20,加锁范围:(10,20] ;
- 第5把锁:行锁,age索引,临键锁,age=30,加锁范围:(20,30] ;
- 第6、7、8把锁:行锁,主键索引,记录锁,加锁范围:id=9、10、11;
-
综上可得出:
- 临键锁 age范围:(10,30] , 记录锁id范围:9、10、11;
- age = (10,30] 不允许插入;age = 30对应的id =12记录加记录锁,不允许编辑、删除;
- id=9、10、11 不允许编辑、删除;
补充例子:
case1:查询不到数据
# age范围查询
select * from user where age between 25 and 28 for update;
# 查询2
select INDEX_NAME,LOCK_TYPE, LOCK_MODE,LOCK_DATA from `performance_schema`.data_locks;
+------------+-----------+-----------+-----------+
| INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_DATA |
+------------+-----------+-----------+-----------+
| NULL | TABLE | IX | NULL |
| idx_age | RECORD | X | 30, 12 |
+------------+-----------+-----------+-----------+
分析:
-
存在的锁
- 第1把锁:表锁;
- 第2把锁:行锁,age索引,临键锁,age=30,加锁范围:(20,30] ;
-
综上可得出:
-
临键锁 age范围:(20,30] ;
-
age = (20,30] 不允许插入;age = 30对应的id =12记录加记录锁,不允许编辑、删除;
-