MySQL 唯一索引下先事务A插入再事务B当前读是否阻塞问题
文章目录
- 唯一索引下先事务A插入再事务B当前读是否阻塞问题
- 场景模拟
- 扩展场景 1:事务 B 当前读 where col > 6
- 扩展场景 2:事务 B 当前读 where col >= 6
- 扩展场景 3:事务 B 当前读 where col >= 5
唯一索引下先事务A插入再事务B当前读是否阻塞问题
场景模拟
场景描述:可重复读隔离级别下,一个唯一索引,有数据 5、9,先事务 A 插入数据 6,然后事务 B 当前读 where col > 7,问事务 B 是否阻塞?
假设有一张 student
表,对其中字段 age
建立唯一索引,并已存在两条 age 分别为 5、9 的记录。
DROP TABLE IF EXISTS student;
CREATE TABLE student(
id INT UNSIGNED AUTO_INCREMENT COMMENT '主键',
age TINYINT UNSIGNED NOT NULL UNIQUE COMMENT '年龄',
PRIMARY KEY(id)
);
INSERT INTO student(age) VALUES
(5),
(9);
之后,有如下流程:
- 先事务 A 插入一条 age = 6 的记录。
- 然后事务 B 当前读 where age > 7。
先事务 A 插入一条 age = 6 的记录。
insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为隐式锁来保护记录的。
当事务需要加锁时,如果这个锁不可能发生冲突,InnoDB 会跳过加锁环节,这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。
每 insert 插入一条新记录,RR 隔离级别下为了防止幻读,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁。
- 如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态。
- 如果没有间隙锁,则无需生成插入意向锁,InnoDB 会跳过加锁环节,也就是隐式锁机制。只有在插入记录之后,特殊情况下,才会将隐式锁转换为显式锁(X 型的记录锁)。
我们来观察一下事务 A 插入一条 age = 6 的记录后的锁状态,执行 select * from performance_schema.data_locks\G;
语句。
可以看到,并不存在行级锁,只有一个表级别的意向独占锁被事务 A 持有,这表明在事务 A 的后续操作中可能会加行级别的独占锁。这其实就是在之前提到的「只有在插入记录之后,特殊情况下,才会将隐式锁转换为显式锁(X 型的记录锁)」。
然后事务 B 当前读 where age > 7。
发现事务 B 并没有阻塞。
再来看一下当前的锁状态,执行 select * from performance_schema.data_locks\G;
语句。略过表级锁和主键加锁情况,这里只看值得关注的。
可以发现事务 A 并没有加行级锁,也就是还是隐式锁状态。而事务 B 加了两个行级锁:
- 在最大虚拟记录 supremum 上,加了 X 型的临键锁,锁住范围 (9, +∞)
- 在索引记录 age = 9 上,加了 X 型的临键锁,锁住范围 (6, 9]
原因分析:
- 当前读加间隙锁的目的是避免幻读,也就是说事务 B 只是希望在范围 (7, +∞) 内没有新增记录,并不需要获取不在自己当前读区间 age=6 的独占锁。
- 无论有没有其他事务先新增、修改、删除、当前读了 age=6 这条记录,都不影响事务 B 对范围 (7, +∞) 的独占统治。
- 事务 B 在加锁时会找到比 7 大的下一条记录,也就是 age=9,给它加 X 型临键锁。
扩展场景 1:事务 B 当前读 where col > 6
场景描述:可重复读隔离级别下,一个唯一索引,有数据 5、9,先事务 A 插入数据 6,然后事务 B 当前读 where col > 6,问事务 B 是否阻塞?
假设有一张 student
表,对其中字段 age
建立唯一索引,并已存在两条 age 分别为 5、9 的记录。
DROP TABLE IF EXISTS student;
CREATE TABLE student(
id INT UNSIGNED AUTO_INCREMENT COMMENT '主键',
age TINYINT UNSIGNED NOT NULL UNIQUE COMMENT '年龄',
PRIMARY KEY(id)
);
INSERT INTO student(age) VALUES
(5),
(9);
之后,有如下流程:
- 先事务 A 插入一条 age = 6 的记录。
- 然后事务 B 当前读 where age > 6。
先事务 A 插入一条 age = 6 的记录。
然后事务 B 当前读 where age > 6。
发现事务 B 并没有阻塞。
再来看一下当前的锁状态,执行 select * from performance_schema.data_locks\G;
语句。略过表级锁和主键加锁情况,这里只看值得关注的。
可以发现事务 A 并没有加行级锁,也就是还是隐式锁状态。而事务 B 加了两个行级锁:
- 在最大虚拟记录 supremum 上,加了 X 型的临键锁,锁住范围 (9, +∞)
- 在索引记录 age = 9 上,加了 X 型的临键锁,锁住范围 (6, 9]
原因分析:
- 当前读加间隙锁的目的是避免幻读,也就是说事务 B 只是希望在范围 (6, +∞) 内没有新增记录,并不需要获取不在自己当前读区间 age=6 的独占锁。
- 无论有没有其他事务先新增、修改、删除、当前读了 age=6 这条记录,都不影响事务 B 对范围 (6, +∞) 的独占统治。
- 事务 B 在加锁时会找到比 6 大的下一条记录,也就是 age=9,给它加 X 型临键锁。
扩展场景 2:事务 B 当前读 where col >= 6
场景描述:可重复读隔离级别下,一个唯一索引,有数据 5、9,先事务 A 插入数据 6,然后事务 B 当前读 where col >= 6,问事务 B 是否阻塞?
假设有一张 student
表,对其中字段 age
建立唯一索引,并已存在两条 age 分别为 5、9 的记录。
DROP TABLE IF EXISTS student;
CREATE TABLE student(
id INT UNSIGNED AUTO_INCREMENT COMMENT '主键',
age TINYINT UNSIGNED NOT NULL UNIQUE DEFAULT 0 COMMENT '年龄',
PRIMARY KEY(id)
);
INSERT INTO student(age) VALUES
(5),
(9);
之后,有如下流程:
- 先事务 A 插入一条 age = 6 的记录。
- 然后事务 B 当前读 where age >= 6。
先事务 A 插入一条 age = 6 的记录。
然后事务 B 当前读 where age >= 6。
发现事务 B 阻塞了。
再来看一下当前的锁状态,执行 select * from performance_schema.data_locks\G;
语句。略过表级锁和主键加锁情况,这里只看值得关注的。
可以发现,事务 A 从隐式锁转为了显示锁,即持有 age=6 的 X 型记录锁状态。而事务 B 需要等待获取 age=6 的 X 型临键锁。
原因分析:
- 当前读加间隙锁的目的是避免幻读,也就是说事务 B 只是希望在范围 [6, +∞) 内没有新增记录,就必须获取在自己当前读区间 age=6 的独占锁。
- 事务 B 在加锁时发现 age=6 这条记录存在,就需要给这条记录加 X 型临键锁。
- 而事务 A 从隐式锁转为了显示锁,持有 age=6 的 X 型记录锁,事务 B 就需要等待事务 A 释放锁,才能给这条记录加 X 型临键锁。
- 事务 B 在当前读下的锁等待与获取保证了不会出现脏读、不可重复读和幻读问题。
扩展场景 3:事务 B 当前读 where col >= 5
场景描述:可重复读隔离级别下,一个唯一索引,有数据 5、9,先事务 A 插入数据 6,然后事务 B 当前读 where col >= 5,问事务 B 是否阻塞?
假设有一张 student
表,对其中字段 age
建立唯一索引,并已存在两条 age 分别为 5、9 的记录。
DROP TABLE IF EXISTS student;
CREATE TABLE student(
id INT UNSIGNED AUTO_INCREMENT COMMENT '主键',
age TINYINT UNSIGNED NOT NULL UNIQUE DEFAULT 0 COMMENT '年龄',
PRIMARY KEY(id)
);
INSERT INTO student(age) VALUES
(5),
(9);
之后,有如下流程:
- 先事务 A 插入一条 age = 6 的记录。
- 然后事务 B 当前读 where age >= 5。
先事务 A 插入一条 age = 6 的记录。
然后事务 B 当前读 where age >= 5。
发现事务 B 阻塞了。
再来看一下当前的锁状态,执行 select * from performance_schema.data_locks\G;
语句。略过表级锁和主键加锁情况,这里只看值得关注的。
可以发现,事务 A 从隐式锁转为了显示锁,即持有 age=6 的 X 型记录锁状态。而事务 B 需要等待获取 age=6 的 X 型临键锁。
原因分析:
- 当前读加间隙锁的目的是避免幻读,也就是说事务 B 只是希望在范围 [5, +∞) 内没有新增记录,就必须获取在自己当前读区间 age=6 的独占锁。
- 事务 B 在加锁时发现 age=6 这条记录存在,就需要给这条记录加 X 型临键锁。
- 而事务 A 从隐式锁转为了显示锁,持有 age=6 的 X 型记录锁,事务 B 就需要等待事务 A 释放锁,才能给这条记录加 X 型临键锁。
- 事务 B 在当前读下的锁等待与获取保证了不会出现脏读、不可重复读和幻读问题。