文章目录
- 1 关于 Record Lock 的实验
- 1.1 实验 1:没有主键时的如何锁定
- 1.2 实验 1(续):带着问题继续实验
- 1.3 实验 2:有主键时如何锁定
- 2 关于 Next-Key Lock 的实验
- 2.1 实验 3:如何确定算法的锁定范围
- 2.2 实验 4:降序索引下,算法的锁定范围
- 2.3 实验 5:幻读实验(Next-Key Lock 与 幻读)
- 3 实验总结与疑问
- 4 参考资料
前言:实验环境:mysql 版本
8.0.26
,其他环境如 5.7 或许有很小的不同但关系不大!实验目的:加深对锁相关理论知识的理解,理解锁理论的自然性😋
作者写这个实验方案花费了不小的精力,希望看官们点赞转发评论点 Stat 支持一下
1 关于 Record Lock 的实验
Record Lock 总是会去锁住索引记录,如果 InnoDB 存储引擎表在建立的时候没有设置任何一个索引,那么这时 InnoDB 存储引擎会使用隐式的主键来进行锁定
1.1 实验 1:没有主键时的如何锁定
实验步骤如下:
- 环境准备
-- 准备表
CREATE TABLE `t1` (
`id` int NOT NULL,
`name` varchar(200) DEFAULT NULL
);
-- 准备数据(id值不要全部连续)
INSERT INTO `t1` ( `id` , `name` ) VALUES (10, 'aaa');
INSERT INTO `t1` ( `id` , `name` ) VALUES (12, 'bbb');
INSERT INTO `t1` ( `id` , `name` ) VALUES (14, 'ccc');
- 开启一个客户端执行
行记录锁定
-- 1、开启一个事务
begin;
-- 2、锁定 id 为 12 的行
select * from t1 where id = 12 for update;
- 观察
performance_schema.data_Lock
表中的内容
观察图中情况:
1、可以看到存在一个表锁和多个行锁
2、关于表锁
- LOCK_TYPE 为 TABLE 表明是表锁
- LOCK_MODE 是
IX
是意向锁。关于意向锁这里不做解释,当其他事务要对全表的数据进行加锁时,那么就不需要判断每一条数据是否被加锁了- 表锁锁定的是全表,所以 LOCK_DATA 是空,INDEX_NAME 为空
3、关于行锁
- LOCK_TYPE 为 RECORD 表明是行锁
- 索引名称
GEN_CLUST_INDEX
是自动生成的聚簇索引(因为你没有指定主键)- 锁定模式 LOCK_MODE 是排它锁
- 在 LOCK_DATA 字段发现把我们插入的 3 条记录全部锁定了,锁定的内容是自动生成的的 RowID。在一个表没有指定主键时,mysql 会为每行生成 RowID,占用 8 个字节,详细内容参考作者另外的文章:《InnoDB 行记录格式(行记录格式实验,重要).md》
- 在 LOCK_DATA 还出现了新的内容
supremum pseudo-record
(上界限伪记录),详细内容参考作者另外的文章:《InnoDB 行记录格式(行记录格式实验,重要).md》
1.2 实验 1(续):带着问题继续实验
问题 1:为什么要锁定全部的记录呢
如果仅仅锁定 id 为 12 的记录,另外一个会话把 id 为 14 的记录改为 id 为 12,这样不就破坏了
for update
排它的语义了
问题 2:如果在别的会话中插入数据可以吗
实验步骤如下:
- 继续实验,另开一个会话,插入任意数据
-- 1、开启另外一个会话
begin;
-- 2、插入数据( id为12 或 id为非12 均可)
INSERT INTO `t1` ( `id` , `name` ) VALUES (20, 'ddd');
- 观察结果和
performance_schema.data_Lock
表中的情况
观察图中情况:
1、在别的会话中插入任何数据都会被阻塞然后超时
2、在 data_Lock
表中出现了新的 2 条记录
先对表加了一个
IX
意向排它锁,这很好理解,insert 操作必然会先对表加意向排它锁然后对某个页中的伪记录
supremum pseudo-record
加了X,INSERT_INTENTION
锁(具体哪一页其实在 MySQL 5.7 中可以通过表 INNODB_Lock 找到,但在 MySQL 8.0 的表 data_Lock 中不再显示),这肯定跟原本给伪记录加的X
锁不兼容,那就肯定会等待啊
问题 3:经过上面 2 个问题,既然别的会话既不能插入也不能锁定任何记录,那为啥不直接锁定表,反而是锁定全表的记录
因为当前会话还是可以修改的,这也算是不加主键的危害了,如果是大表将导致非常多的锁记录被生成,很消耗心内存,危!!!
1.3 实验 2:有主键时如何锁定
实验步骤如下:
- 环境准备
-- 准备表
CREATE TABLE `t1` (
`id` int NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY ( `id` )
);
-- 准备数据(id值不要全部连续)
INSERT INTO `t1` ( `id` , `name` ) VALUES (10, 'aaa');
INSERT INTO `t1` ( `id` , `name` ) VALUES (11, 'bbb');
INSERT INTO `t1` ( `id` , `name` ) VALUES (13, 'ccc');
INSERT INTO `t1` ( `id` , `name` ) VALUES (20, 'ddd');
- 开启一个客户端执行
行记录锁定
-- 1、开启一个事务
begin;
-- 2、锁定
select * from t1 where id = 11 for update;
- 观察
performance_schema.data_Lock
锁定的内容
观察图中情况:
1、索引名称为主键索引
2、先对表加了
IX
意向锁3、锁定的数据是就是主键数据,而不是自动生成的 RowID
2 关于 Next-Key Lock 的实验
Record Lock:记录锁,锁定的是索引记录
Gap Lock:间隙锁,作用在索引之间的间隙上,或者第一条记录之前,或者最后一条记录之后
Next-Key Lock 是 Gap Lock 和 Record Lock 的结合
先来点理论介绍吧:
1、Next-Key Lock 是结合了 Gap Lock 和 Record Lock 的一种锁定算法,在 Next-Key Lock算法下,InnoDB 对于行的查询都是采用这种锁定算法
2、仅在查询的列是唯一索引的情况下,Next-Key Lock降级为Record Lock,即仅锁住索引本身,而不是范围
3、而对于辅助索引,其加上的是 Next-Key Lock,锁定的范围是索引的左边间隙和右边间隙至于是否包含边界看索引是升序或降序(看实验)
2.1 实验 3:如何确定算法的锁定范围
提示:本实验针对插入操作,删除修改操作在边界问题有轻微区别
实验步骤如下:
- 环境准备(索引 idx_b 是普通索引不是唯一索引)
-- 1、准备表
CREATE TABLE `t1` (
`a` int NOT NULL,
`b` int DEFAULT NULL,
PRIMARY KEY ( `a` ),
KEY `idx_b` ( `b` )
);
-- 2、插入记录(b字段不要连续)
insert into t1 select 1,1;
insert into t1 select 3,1;
insert into t1 select 5,3;
insert into t1 select 7,6;
insert into t1 select 10,8;
- 开启一个客户端执行
行记录锁定
,并关注锁情况
begin;
select * from t1 where b = 3 for update;
观察图中情况:
一共有 2 个索引,一个是主键索引,一个是普通索引
1、对主键索引(聚簇索引),其仅仅对 a=5 的索引加上
X,REC_NOT_GAP
2、而对于辅助索引 idx_b,使用 Next-Key Lock 技术加锁
3、而对于辅助索引 idx_b,使用 Next-Key Lock 技术加锁。锁定的范围是该记录左边右边的空隙,至于是否包含某一边的边界要看索引是升序排序还是降序,对本文因为索引是升序的,所以最终的锁定范围是:
(1,6) + 1
,合并为[1,6)
- 另起一个会话执行如下操作,观察是否能插入成功
-- 1、另外开一个会话
begin;
-- 2、既然是间隙锁,那就有必要对 b=3 的前前后后的临界情况进行『`分别`』测试
insert into t1 select 20,0; -- (OK)
insert into t1 select 21,1; -- (阻塞)X,GAP,INSERT_INTENTION
insert into t1 select 22,2; -- (阻塞)X,GAP,INSERT_INTENTION
insert into t1 select 23,3; -- (阻塞)X,GAP,INSERT_INTENTION
insert into t1 select 24,4; -- (阻塞)X,GAP,INSERT_INTENTION
insert into t1 select 25,5; -- (阻塞)X,GAP,INSERT_INTENTION
insert into t1 select 26,6; -- (OK)
insert into t1 select 27,7; -- (OK)
分析上表的实验结果:
1、b=3 不能插入好理解,如果能插入肯定影响别的会话
2、b 在区间 (1,3)
和 (3,6)
不能插入也好理解,因为索引 idx_b
是排序的且不是唯一索引,所以根本没有办法知道在该条记录的左边右边是否有或有几条 b=3 的数据,那么只好退而求其次锁定该记录左右的空隙,即锁定 (1,3)
和 (3,6)
3、至于边界情况,即 1 或 6 是否应该允许插入,要根据索引是升序还是降序来确定
-
索引升序(升序情况下,索引值小的排列在前面索引值大的排列在后面)
如果插入 b=6 索引必然会出现在 b=6 位置后面,与现有的间隙不矛盾,所以边界 b=6 可以插入
如果插入 b=1 索引必然会出现在 b=1 位置后面,与现有的间隙矛盾,所以边界 b=1 不可以插入
-
索引降序(降序情况下,索引值大的排列在前面索引值小的排列在后面)
如果插入 b=6 索引必然会出现在 b=6 位置后面,与现有的间隙矛盾,所以边界 b=6 不可以插入
如果插入 b=1 索引必然会出现在 b=1 位置后面,与现有的间隙不矛盾,所以边界 b=1 可以插入
综上,针对插入操作,合并得到插入操作的锁定区间是: [1,6)
2.2 实验 4:降序索引下,算法的锁定范围
提示:本实验针对插入操作,删除修改操作在边界问题有轻微区别
实验步骤如下:
-
环境准备(注意:idx_b 索引是降序的)
-- 1、准备表 CREATE TABLE `t1` (
a
int NOT NULL,
b
int DEFAULT NULL,
PRIMARY KEY ( a
),
KEY idx_b
( b
DESC)
);
– 2、插入记录(b字段不要连续)
insert into t1 select 1,1;
insert into t1 select 3,1;
insert into t1 select 5,3;
insert into t1 select 7,6;
insert into t1 select 10,8;
2. 开启一个客户端执行 `行记录锁定`
```mysql
begin;
select * from t1 where b = 3 for update;
-
另起一个会话执行如下操作,观察是否能插入成功
-- 1、另外开一个会话 begin; -- 2、既然是间隙锁,那就有必要对 b=3 的前前后后的临界情况进行『`分别`』测试 insert into t1 select 20,0; -- (xxx) insert into t1 select 21,1; -- (xxx) insert into t1 select 22,2; -- (xxx) insert into t1 select 23,3; -- (xxx) insert into t1 select 24,4; -- (xxx) insert into t1 select 25,5; -- (xxx) insert into t1 select 26,6; -- (xxx) insert into t1 select 27,7; -- (xxx)
后面步骤留给读者
补充上表的结果,并得出降序索引下算法的锁定范围是什么
2.3 实验 5:幻读实验(Next-Key Lock 与 幻读)
什么是幻读:事务A读取与搜索条件相匹配的若干行,但事务B以插入或删除行等方式来影响了事务A的结果集
例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样
一般避免幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读
既然 Next-Key Lock 算法锁定了记录左右的间隙,且是排它方式锁定的,是不是就意味着该间隙区间内既不能插入也不能删除或修改
实验步骤如下:
-
环境准备并设置隔离级别为 RR
-- 1、准备表 CREATE TABLE `t1` (
a
int NOT NULL,
b
int DEFAULT NULL,
PRIMARY KEY ( a
),
KEY idx_b
( b
)
);
– 2、插入记录
insert into t1 select 2,1;
insert into t1 select 4,3;
insert into t1 select 6,5;
insert into t1 select 8,7;
insert into t1 select 10,9;
– 3、设置隔离级别为 RR
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
– 4、查看隔离级别
SELECT @@SESSION.transaction_isolation;
2. 开启一个客户端执行 `行记录锁定` ,并关注锁情况
```mysql
begin;
select * from t1 where b = 5 for update;
-
猜测实验结果
根据实验 3 的经验来看:
1、插入时锁定的索引范围是:
[3,7)
,左闭右开,这也很好理解,插入时毕竟会插入索引嘛,但是删除或修改时是不是或许边界会有不同2、更新边界 b=3 预测会成功,但是如果把 b 更新为 b=5 呢,不也破坏了 for update 的语义了,所以结果不一定
3、如果删除了 b=3 那么间隙不就变了吗,所以结果也不一定
begin; delete from t1 where b = 3; -- (预测会成功) update t1 set b = 5 where b = 3; -- (预测只要不把 b 更新为 3 就会成功) update t1 set b = 11 where b = 3; -- (预测只要不把 b 更新为 3 就会成功) update t1 set b = 12 where b = 5; -- (预测肯定阻塞) rollback;
-
既然已经预测了结果,那么另起一个会话执行如下操作,观察是情况
begin; delete from t1 where b = 3; -- (OK) update t1 set b = 5 where b = 3; -- (阻塞) update t1 set b = 11 where b = 3; -- (OK) update t1 set b = 12 where b = 5; -- (阻塞) rollback;
😁😄,全部预测成功,good boy!
作者看了一眼
data_locks
多了很多记录,相当复杂反正我也看不懂但是没有关系,算法很复杂但是很符合常识,根本不需要懂,嘿嘿 -
既然可以删除边界 b=3,但是删除后明显间隙就变了呀,Next-Key Lock 算法机制是不是也认为间隙也变了,间隙是动态的呢
删除边界 b=3 后,新的边界变成了 b=1,那么对于插入会改变索引的操作而言是不是插入 b=1 会失败呢
-- 1、删除 b=1 并提交 begin; delete from t1 where b = 3; -- (OK) commit; -- 2、测试边界是不是已经变成了 0,然后 0 插入失败 begin; insert into t1 select 3,1; -- (预测会阻塞) rollback;
还是预测成功了,间隙锁的间隙是动态变化的过程
实验结果:
Next-Key Lock 算法通过锁定间隙区间避免了幻读问题,在间隙区间内不允许插入、删除、修改,对这些操作会阻塞,自然也就避免了幻读问题
3 实验总结与疑问
实验结果:
实验 1:
1、不加主键索引危害多,如果是大表哪怕随便锁定一行也会导致全表的记录被锁定,会创建大量的锁记录,很很大不良影响
2、因为没有唯一约束,所以哪怕对任意一行加行锁也会锁定所有记录,而且还不允许别的会话插入
实验 2:
1、加主键索引好处多(加行锁时只锁定一行不影响其他行)
实验 3 和实验 4:
1、一般情况的,可以理解 Next-Key Lock 算法锁定的索引范围是:
(m,n)
。至于是否包含边界具体看情况,影响了锁定的间隙就不行,一般你的感觉就是准的,所以是否包含边界就不重要了2、对算法的理解重点是对于『
间隙
』的理解,其实就是字面意思,一个索引范围。这个范围是动态的,如何理解,如发生了数据删除自然间隙就变大了
实验 5:
Next-Key Lock 算法通过锁定一段索引间隙区间,在区间内的操作都是排它的,区间内不允许插入、删除、修改,从而避免了幻读问题
总结:
1、实验目的在于加强对理论的理解,实验完发现一切就是那么自然根本不需要记忆
2、锁它锁定的标的是什么(猜到了吧:就是索引,具体来说就是索引中的 LOCK_DATA
)
3、理解 Next-Key Lock 算法的关键是对『间隙
』的理解,本质上是锁定了一段索引区间,只要不干涉该索引区间的操作都是被允许的,干涉该索引区间的操作都是不被允许的
4、幻读的一般避免方案就是范围锁,锁定的范围内只允许读,从而避免了幻读
4、Next-Key Lock 算法通过锁定索引间隙,使其它客户端不可以插入、修改、删除该间隙内的内容,从而避免了幻读问题
疑问:
1、从实验 3 的结果来看,锁定的区间是: [1,6)
,但是在上图中并没有很好的体现出锁定的范围,那么应该怎么理解 LOCK_MODE 与 LOCK_DATA 呢?
2、LOCK_MODE 的 X
不像是表达『排它锁
』的含义,而有点『范围锁
』的味道
作者也找了一些资料,并没有很好的对 LOCK_MODE 的说明,各位看官如知道欢迎联系交流
4 参考资料
官网: https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html
官网(data_locks 表): https://dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html
书籍:《InnoDB 存储引擎》
传送门: 保姆式Spring5源码解析
欢迎与作者一起交流技术和工作生活
联系作者