生成间隙(gap)锁、临键(next-key)锁的前提条件 是在 RR 隔离级别下
先给出结论:
当Mysql的事务隔离级别是 REPEATABLE READ
间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据,这是为了防止出现 幻读 现象;
间隙的范围?
根据检索条件向下寻找最靠近检索条件的记录值A作为左区间,向上寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为(A,B] 左开右闭。
#查看正在被锁定的的表 show OPEN TABLES where In_use > 0; # 查看进程号 show processlist; #查看正在锁的事务 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; #查看等待锁的事务 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; #查看系统事务隔离级别 select @@global.tx_isolation; #查看当前事务 select @@tx_isolation; #设置全局事务隔离级别为RC set global transaction isolation level repeatable read; #设置当前事务隔离级别为RC set session transaction isolation level read committed;
测试环境:
MySQL版本5.7.27
事务隔离级别:Repeatable Read
CREATE TABLE `t` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', `age` int NOT NULL COMMENT '年龄', `mobile` int DEFAULT NULL COMMENT '手机号', `name` varchar(8) DEFAULT NULL COMMENT '名称', PRIMARY KEY (`id`), KEY `index_age` (`age`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
id为主键(唯一索引)、age是普通索引、mobile没有加索引
插入数据如下:
id (主键) | age(普通索引) | mobile(无索引) | name |
---|---|---|---|
1 | 1 | 18142219401 | Andew1 |
4 | 4 | 18142219404 | Andew4 |
7 | 7 | 18142219407 | Andew7 |
在进行测试之前,我们先来看看t表中存在的隐藏间隙:
(-∞, 1]
(1, 4]
(4, 7]
(7, +supernum]
(其中supernum是数据库维护的最大的值。为了保证间隙锁都是左开右闭原则。)
打开mysql 的连接客户端navicate ,每打开一个查询窗口就是一个事务
mysql> SHOW VARIABLES LIKE 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.00 sec)
结果显示,autocommit 的值是 ON,表示系统开启自动提交模式。
在 MySQL 中,可以使用 SET autocommit 语句设置事务的自动提交模式,语法格式如下:
SET autocommit = 0|1|ON|OFF
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | begin |
2 | select * from t where id=4 for update; | - |
3 | select * from t where id=4 for update; | |
4 | commit | 阻塞 |
事务1等值查询id=4,因为id是主键,同时是等值查询存在该记录,所以只会在id=4这条记录上加记录锁,不会加间隙锁。
事务2 等值查询id=4,这个时候会产生行锁
#查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
在事务2的MySQL控制台上会报错:
1205 - Lock wait timeout exceeded; try restarting transaction
事务字段解释:
lock_id: 锁的id lock_trx_id: 事务的ID lock_mode: 锁的模式 lock_type: 锁的类型,表锁还是行锁 lock_table: 要加锁的表 lock_index: 锁住的索引 lock_space: 锁住对象的 space_id lock_page: 事务锁住页的数量,若是表锁,则为null lock_rec: 被锁的记录号 lock_data: 事务锁定记录的主键值,若是表锁,则该值为null
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | begin |
2 | select * from t where id=4 for update; | - |
3 | select * from t where id=5 for update; | |
4 | commit | 不阻塞 |
事务1等值查询id=4,因为id是主键,同时是等值查询存在该记录,所以只会在id=4这条记录上加记录锁,不会加间隙锁。
事务2 等值查询id=5,没有锁冲突,所以查询正常,不会堵塞。(如果事务B 等值查询id=4,因为事务A加了记录锁,所以会堵塞),但是事务2等值查询id=5,因为查询记录不存在,所以无法加记录锁,但这里会存在一个(5,7]的间隙锁。
示例如下:
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | begin |
2 | select * from t where id=5 for update; | - |
3 | - | INSERT INTO t (id , age , mobile , name ) VALUES (6, 6, 122, ‘安路6’); |
4 | commit | 不阻塞 |
事务1 等值查询id=5,因为查询记录不存在,所以无法加记录锁,但这里会存在一个(5,7]的间隙锁。
事务2 插入一条id=6的数据,因为上面存在了(5,7]的间隙锁,所以会堵塞
查看当前mysql 事务锁
在事务2的MySQL控制台上会报错:
1205 - Lock wait timeout exceeded; try restarting transaction
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | begin |
2 | select * from t where id>4 for update; | - |
3 | - | INSERT INTO t (id , age , mobile , name ) VALUES (6, 6, 122, ‘安路6’); |
4 | commit | 阻塞 |
事务1查询id大于4的记录,会生产一个(4,+supernum]的临键(next-key)锁
事务2 插入一个id=6、age=6的数据,因为age值在上面临键锁,范围内,所以也会堵塞。
如果 事务B 是更新 id=7 的记录,同样会堵塞
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | begin |
2 | select * from t where age=4 for update; | - |
3 | - | INSERT INTO t (id , age , mobile , name ) VALUES (6, 6, 122, ‘安路6’); |
4 | commit | 阻塞 |
事务1 等值查询age=4,因为age是普通索引,所以会产生临键(next-key)锁(1,4]和(4,7],左开右闭原则。
事务2 插入一个id=6、age=6的数据,因为age值在上面临键锁,范围内,所以也会堵塞
按照上面的例子,如果事务2插入一条 id=6,age=1 的数据会不会堵塞呢,因为按照左开右闭原则,上面的age=1是开的,所以正常应该是可以插入的。
但实际上你真是实践之后,你发现同样也会堵塞。
通过实践之后,会发现,所谓的左开右闭原则,跟主键id有关系。
上面的事务1 等值查询age=4,它的当前主键id=4,上一条记录主键id=1,下条记录主键id=7。
如果插入 id<1, age 在(1,7)范围内,是 左闭右开原则。即age=1能插入,age=7会堵塞。
如果插入 1<id<7,age 在(1,7)范围内,是 左闭右闭原则。即age=1会堵塞,age=7也会堵塞。
如果插入 id>7,age 在(1,7)范围内,是 左开右闭原则。即age=1会堵塞,age=7能插入
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | begin |
2 | select * from t where mobile=‘18142219404’ for update; | - |
3 | - | INSERT INTO t (id , age , mobile , name ) VALUES (100, 100, ‘18142219500’, ‘安路8’); |
4 | commit | 阻塞 |
事务1 等值查询 mobile = 18142219404,因为mobile是无索引的,所以这个for update,变成表级排他(X)锁。
事务2 因为事务A已经加了表级的排他锁,所以其它事务无法进行任何的增删改操作。
查看当前MySQL锁
这是表中所有行加锁的效果,而不是锁表,不过效果一样,表中数据是无法操作的
步骤 | 事务1 | 事务2 |
---|---|---|
1 | begin | begin |
2 | select * from t where mobile=‘18142219404’ for update; | - |
3 | - | INSERT INTO t (id , age , mobile , name ) VALUES (100, 100, ‘18142219500’, ‘安路8’); |
4 | commit | 阻塞 |
根据唯一索引、非唯一索引和无索引,以及数据库中是否存在查询边界,进行测试。结果如下,其中边界5存在索引中,边界6不在。
结论:
凡是涉及范围锁,都会在行记录上加Next-Key Lock(X和GAP锁)。其中,GAP间隙锁,防止幻读现象(一个事务中后边读的行比前一次多)
小于时,唯一索引和非唯一索引加锁最大范围,都是索引中最接近边界的下一个索引,并且包含该索引(Next-Key),如索引5,和离边界6最近的索引9;
大于时,唯一索引和非唯一索引加锁最小范围,都是索引中最接近边界的前一个索引,但不包含该索引,如索引5,和离6最近的索引5;
如果插入的值超过索引中最大值,锁类型变为X锁,被锁的最大行为supremum pseudo-record,它具有大于所有索引的值,不是真正的索引记录。此时,锁的范围扩大到正无穷。
① 唯一索引,在索引中存在该值,锁由Next-Key Lock降级为Record锁,只锁住该行;索引中不存在边界值,锁的范围为离边界值最近的两个索引,但不包含这两个索引,如5和9;
② 非唯一索引,无论索引中存在该边界值与否,都会找离边界值最近的两个索引(边界值在索引中,也会去前边一个索引,如5,会取3)
无索引情况下,对全表行加锁。
InnoDB存储引擎中底层锁定的实际范围会根据插入值而变化,给用户感觉锁情况为上表。
如果删除范围锁中的上下边界索引(当然是未被锁定的索引),范围锁会动态地将锁的范围扩大到下一个索引位置。
在唯一索引或非唯一索引上加锁时,还会在对应聚集索引上的主键加锁,防止通过索引修改记录和通过主键修改记录冲突