前言
我们这里主要是 来看一下 mysql 中的 间隙锁
间隙锁 主要存在的地方一般就是在 查询主键查询不到, 索引查询查询不到 的场景
然后 我们这里来调试一下 这里的整个流程, 间隙锁的加锁 以及 间隙锁的使用, 以及 间隙锁的释放
从逻辑上来说 间隙锁 锁定的是一个区间, 按照我们常规的理解 他应该会保存 区间的起始地址, 但是 从实际的实现层面 mysql 这边的实现相当巧妙, 它是挂在比目标值大的下一条记录上面的, 比如主键有 1, 5, 10, 我们这里执行查询 “select * from tz_test_04 where id = 7 for update;” 间隙锁是挂在 id 为 10 的这条记录上面的一个行锁, 并且 增加了一个 GAP_LOCK 的标志, 用于 业务的判断
根据这个目标记录 10, 以及 10 的上一条记录 5, 改间隙锁推导出锁定的区间就是 (5, 10)
我们这里测试表结构如下
CREATE TABLE `tz_test_04` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`field1` varchar(128) DEFAULT NULL,
`field2` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `field_1_2` (`field1`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8
测试数据如下, 之所以 留下一些区间 是为了产生 “间隙”
添加间隙锁
为了演示, 我们这里 事务1 加锁 sql 如下
此查询会在 id 为 10 的行上面增加一个行锁, 并打上 GAP_LOCK 标志, 逻辑意义上这个间隙锁锁定的区间为 主键的 (5, 10)
因此 我们在这个区间去 增加记录 会获取 行排他锁, 获取失败, 如果是 更新或者删除, 还不到获取行排他锁锁的地方, 因此可以正常执行
begin;
select * from tz_test_04 where id = 7 for update;
-- sleep10min
commit;
获取锁的地方如下, mode 为 515 = LOCK_GAP + LOCK_X, 间隙排他锁
锁定的记录的 heap_no 为 5, 上下文还有它的 space, page 的相关信息
在 search_mvcc 中上下文如下, 是在 cmp_dtuple_is_prefix_of_rec 的判断内部, 条件匹配不上的时候, 会添加这个间隙锁
会进入这里的比较, 我这边目前知道的有 等于 比较
我们再来看一下 rec 的记录信息
目标记录的上下文信息如下, 可以看到上一次更新是 事务39531 处理的, 当前事务是 39537, 然后对应的记录是 id 为 10 的记录
间隙锁的使用
间隙锁区间新增记录
然后在 事务2 执行sql 如下, 然后可以看到 产生了阻塞
INSERT INTO `test_02`.`tz_test_04`(`id`, `field1`, `field2`) VALUES (8, 'field1', '1');
这里是判断 上下文是否有 存在冲突的锁是否被其他事务持有, 锁住的事务是 事务39537 即上面那副截图的事务, 然后当前事务是 事务39538, 是由 mysql 这边新分配的一个事务
然后期望锁定的目标记录是 5 号记录, 结合上面的信息来看 是 id 为 10 的记录
持有锁的事务的 mode 为 547 为 LOCK_GAP + LOCK_RECORD + LOCK_X
当前事务尝试获取的 mode 为 2563 为 LOCK_INSERT_INTENSION + LOCK_GAP + LOCK_X
然后这里 持有锁的上下文 和 尝试获取锁的上下文 是不兼容的, 因此 当前 事务 只能挂起, 等待目标 事务 释放间隙锁
我们再来看一下 这里的 heap_no 为什么会是 id 为 10 的记录的 heap_no 呢?
获取锁的时候是在插入数据记录的时候, 获取当前记录的下一条记录, 比如我们这里插入的记录 id 为 8, 那么是应该放在 id 为 5 和 id 为 10 的记录之间, 下一条记录即为 id 为 10 的记录
然后 因此就巧妙的实现了 间隙锁, 锁是加载一个行记录上面的, 但是实际锁定的却是一个区间
间隙锁区间更新记录
然后在 事务2 执行sql 如下, 然后可以看到 不会产生阻塞
update tz_test_04 set field1 = 'fieldUpdated' where id = 8;
这是因为如下判断锁是否兼容的时候, 这里在 间隙锁的第二个 case, 这里满足, 就是持有锁的事务是间隙锁, 并且当前事务没有 LOCK_INSERT_INTENSION, 我们这里是更新, 是满足条件的, 因此 视为兼容
间隙锁区间删除记录
然后在 事务2 执行sql 如下, 然后可以看到 不会产生阻塞
delete from tz_test_04 where id = 8;
这是因为如下判断锁是否兼容的时候, 这里在 间隙锁的第二个 case, 这里满足, 就是持有锁的事务是间隙锁, 并且当前事务没有 LOCK_INSERT_INTENSION, 我们这里是删除, 是满足条件的, 因此 视为兼容
间隙锁的释放
释放同样是在 事务提交 的时候
间隙锁锁索引的情况
tz_test_04 的数据表数据如下, 这里更新了一下 id 为 10 的记录的 field1 的值, 让其在字符串层面增量排序
执行 sql 如下
begin;
select * from tz_test_04 where field1 = 'field7' for update;
-- sleep10min
commit;
走的索引查询, 同样如果是 索引字段 field1 没有命中的情况, 会基于索引记录 添加间隙锁
从 rec, 或者代码所在区域 可以看出当前 rec 实际上时索引记录, 然后下面会走 查询是否匹配, 匹配的话走聚簇索引的回表查询, 这里我们的 事务是 事务39576
执行 sql 如下 “INSERT INTO `test_02`.`tz_test_04`(`id`, `field1`, `field2`) VALUES (8, 'field8', '8');”
会添加 数据记录, 索引记录, 这里阻塞是阻塞在插入索引记录的地方
当前 事务39577 尝试获取 “field8” 的下一个索引记录 “field9” 的间隙锁 mode 为 2563, LOCK_INSERT_INTENSION + LOCK_GAP + LOCK_X
发现 事务39576 持有 “field9” 的间隙锁 mode 为 547, LOCK_GAP + LOCK_RECORD + LOCK_X
然后当前事务 有LOCK_INSERT_INTENSION 和 已有的间隙锁不兼容, 因此当前 事务 需要等待
间隙锁锁非索引记录的情况
执行 sql 如下
begin;
select * from tz_test_04 where field2 = '7' for update;
-- sleep10min
commit;
这个效果就转换为了 在每一行记录上面 加上行临键锁
上下文在这里, 具体遍历了每一行记录, 以及 supremum
遍历的各个记录如下, 下图是在 row_search_mvcc 中查看的 rec 的记录信息, 可以通过 field1 字段观察出那一条记录
间隙锁锁最大值到正无穷的情况
间隙锁锁在主键上面
执行 sql 如下
begin;
select * from tz_test_04 where id = 15 for update;
-- sleep10min
commit;
这里是遍历到了 supremum 记录, 然后这里会在 supremum 记录上面添加一个 行排他锁
但是它的作用和在 supremum 记录上面增加一个 间隙锁的效果 是一样的
然后执行 sql 如下 “INSERT INTO `test_02`.`tz_test_04`(`id`, `field1`, `field2`) VALUES (15, 'fieldc', 'c');”
然后这里可以看到是在添加数据记录的时候尝试获取锁 mode 为 2563, LOCK_INSERT_INTENSION + LOCK_GAP + LOCK_X
然后已经持有的事务 mode 为 LOCK_RECORD + LOCK_X
然后 两个事务的情况不在下面的四种兼容情况内, 因此 本事务需要阻塞
需要注意的是这里在 supremum 记录上面增加的是 行排他锁, 而不是 间隙锁
间隙锁锁在索引上面
执行 sql 如下
begin;
INSERT INTO `test_02`.`tz_test_04`(`id`, `field1`, `field2`) VALUES (8, 'fieldc', 'c');
-- sleep10min
commit;
这里是遍历到了 supremum 记录, 然后这里会在 supremum 记录上面添加一个 行排他锁
但是它的作用和在 supremum 记录上面增加一个 间隙锁的效果 是一样的
然后执行 sql 如下 “INSERT INTO `test_02`.`tz_test_04`(`id`, `field1`, `field2`) VALUES (8, 'fieldc', 'c');”
然后这里可以看到是在添加索引记录的时候尝试获取锁 mode 为 2563, LOCK_INSERT_INTENSION + LOCK_GAP + LOCK_X
然后已经持有的事务 mode 为 LOCK_RECORD + LOCK_X
然后 两个事务的情况不在下面的四种兼容情况内, 因此 本事务需要阻塞
需要注意的是这里在 supremum 记录上面增加的是 行排他锁, 而不是 间隙锁
完