临键锁-学习

398 阅读8分钟

结论

image.png

简介

记录锁:锁定的是某一行一级;
间隙锁: 锁定的是记录与记录之间的空隙,间隙锁只阻塞插入操作;
临键锁: 是记录锁、间隙锁(与前一条数据的间隙)的并集,是mysql加锁的基本单位;
mysql会根据不同索引的不同查询情况,进行锁的切换

临键锁为了解决「可重复读」下「当前读」中出现的幻读问题;

如何防止幻读:

  1. 防止间隙内有新数据被插入。(间隙锁)
  2. 防止已存在的数据,更新成间隙内的数据。(记录锁)

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. 存在的锁。

    1. 第1把锁:表锁,目的是如果有对表加锁需求的话,不需要循环判断每行的锁数据;
    2. 第2把锁:行锁,主键索引,id=10;
  2. 临键锁变为记录锁;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. 存在的锁

    1. 第1把锁:表锁(可忽略);
    2. 第2把锁:行锁,主键索引,间隙锁,id=7,加锁范围:(1,7);
  2. 加锁范围:间隙锁(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. 存在的锁

    1. 第1把锁:表锁(忽略)
    2. 第2把锁:主键索引,记录锁,id=10;
    3. 第3把锁:主键索引,临键锁,id=11,加锁范围: (10,11],退化为记录锁 ;
    4. 第4把锁:主键索引,临键锁,id=12,加锁范围: (11,12],退化为记录锁 ;
  2. 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. 存在的锁

    1. 第1把锁:表锁(忽略);
    2. 第2把锁:主键索引,临键锁,id=15,加锁范围:(12,15];
    3. 第3把锁:主键索引,临键锁,id=20,加锁范围:(15,20];
    4. 第4把锁:主键索引,间隙锁,id=56,加锁范围:(20,56);
  1. 通过上述锁可以得出:

    1. 行锁:id=15、id=20,这两条数据不可编辑、删除;
    2. 间隙锁:(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. 存在的锁;

    1. 第1把锁:表锁(忽略);
    2. 第2把锁:主键索引,间隙锁,id=56,加锁范围:(20,56);
  2. 通过上述可以得出:(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. 存在的锁

    1. 第1把锁:表锁(忽略);
    2. 第2、3、4把锁:行锁,age索引,临键锁,age=20,加锁范围:(10,20];
    3. 第5、6、7把锁:行锁,主键索引,记录锁,加锁范围:id=9、10、11;
    4. 第8把锁:行锁,age索引,间隙锁,age=30,加锁范围: (20,30) 【规则一】;
  1. 综上可得出:

    1. 间隙锁范围: (10,30),记录锁范围:id=9、10、11;

    2. 无法更新、删除 id= 9,10,11 或 age = 20 的数据;

    3. 无法插入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. 存在的锁;

    1. 第1把锁:表锁(忽略);
    2. 第2把锁:行锁,间隙锁,age=30,加锁范围: (20,30)
  1. 综上可得出

    1. 间隙锁范围: (20,30);
    2. 无法插入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. 存在的锁

    1. 第1把锁:表锁;
    2. 第2、3、4把锁:行锁,age索引,临键锁,age=20,加锁范围:(10,20] ;
    3. 第5把锁:行锁,age索引,临键锁,age=30,加锁范围:(20,30] ;
    4. 第6、7、8把锁:行锁,主键索引,记录锁,加锁范围:id=9、10、11;
  1. 综上可得出:

    1. 临键锁 age范围:(10,30] , 记录锁id范围:9、10、11;
    2. age = (10,30] 不允许插入;age = 30对应的id =12记录加记录锁,不允许编辑、删除;
    3. id=9、10、11 不允许编辑、删除;

补充例子:

image.png

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. 存在的锁

    1. 第1把锁:表锁;
    2. 第2把锁:行锁,age索引,临键锁,age=30,加锁范围:(20,30] ;
  2. 综上可得出:

    1. 临键锁 age范围:(20,30] ;

    2. age = (20,30] 不允许插入;age = 30对应的id =12记录加记录锁,不允许编辑、删除;