Insert intention lock探索

本文通过实例探讨了MySQL中插入锁的行为,特别是涉及到无主键和有主键表的情况。作者发现,插入锁的锁定范围与Next-Key Lock的描述一致,但边缘值的锁定情况存在特殊情况。在无主键表中,插入可能导致隐式主键的冲突,而在有主键的表中,插入冲突受主键值的影响。通过对不同索引值的分析,作者得出了一种关于冲突判断的公式,揭示了主键如何影响插入锁的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近做了一个插入锁的尝试,感觉很奇怪,这里展示一下并写出自己的分析结果。
先创建一个表格:

CREATE TABLE `t2` (
  `id` int(11) DEFAULT NULL,
  `class` int(11) DEFAULT NULL,
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

插入一些数据:

select * from t2;
+------+-------+
| id   | class |
+------+-------+
|    2 |     2 |
|   10 |    10 |
+------+-------+

先开启一个session作为事务1:

begin;
select * from t2 where id>4 and id<5 for update;

再开启另一个session作为事务2:

begin;
select * from t2 where id=2 for update;--正常
select * from t2 where id=5 for update; --正常,gap和rec锁不冲突
select * from t2 where id=10 for update;--阻塞

能看到这里锁住的范围是(2,10],与next-key lock描述的锁范围一致。
接下来我们试一下insert:

insert into t2 values(2, 2);--阻塞
insert into t2 values(10, 10);--正常

刚好和select for update相反。但这只是表面现象,并不是真正的情况。
我们再创建一个带有主键的表:

CREATE TABLE `t3` (
  `id` int(11) NOT NULL,
  `phone` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

带有同样的数据:

select * from t3;
+------+-------+
| id   | class |
+------+-------+
|    2 |     2 |
|   10 |    10 |
+------+-------+

事务1保持不变,用事务2再试一下insert:

insert into t3 values(2, 2);--主键冲突
insert into t3 values(10, 10);--阻塞

主键冲突其实并不是锁冲突,而是没有冲突后,进入了主键检查,导致了主键冲突。
可以看到这个时候又满足next-key lock了,也就是锁住的范围是(2,10],这就太奇怪了。
后来经过了一天的思考,感觉可能是t2表里隐藏主键在捣鬼,在Innodb里,如果没有设置主键,会默认创建一个聚簇索引的主键,所以我决定自己创建一个有主键的表,自己控制主键的情况:

CREATE TABLE `t4` (
  `id` int(11) NOT NULL,
  `inx` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `inx` (`inx`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

同样的数据:

+----+------+
| id | inx  |
+----+------+
|  2 |    2 |
| 10 |   10 |
+----+------+

事务1:

begin;
select * from t4 where inx>4 and inx<5 for update;

事务1不动了,后面的都是在事务2里进行:

begin;
insert into t4 values(2, 2);--主键冲突
insert into t4 values(10, 10);--阻塞

这个结果跟t3含有主键并锁主键的的表的结果一致,但是继续测试,奇怪的事情发生了:

insert into t4 values(1, 2);--成功
insert into t4 values(3, 2);--阻塞
insert into t4 values(11, 2);--阻塞
insert into t4 values(100, 2);--阻塞

也就是这里当inx=2时,锁住了主键id(2, +inf)的范围,但是

insert into t4 values(11, 11);--又成功

我从头检查了一下next-key lock的范围:

select * from t4 where id=2 for update;--正常
select * from t4 where id=10 for update;--阻塞
select * from t4 where inx=2 for update;--正常
select * from t4 where inx=10 for update;--阻塞
select * from t4 where id>2 and id< 3 for update;--阻塞
select * from t4 where id>9 and id< 10 for update;--阻塞
select * from t4 where inx>2 and inx< 3 for update;--阻塞
select * from t4 where inx>9 and inx<10 for update;--阻塞
select * from t4 where inx>10 and inx<11 for update; --正常
select * from t4 where id>10 and id<11 for update;--正常
select * from t4 where id>1 and id<2 for update; --正常
select * from t4 where inx>1 and inx<2 for update;--正常

可以确定next-key lock的范围:inx的(2, 10],id的(2, 10],这个是正确的。关键是对于边缘值2和10,这两个值什么时候被锁什么时候不被锁,现在比较奇怪,索引再来看看10的值:

insert into t4 values(11, 10);--正常
insert into t4 values(1, 10);--阻塞
insert into t4 values(10, 10);--阻塞

于是发现一个规律:
当inx=2时,id>inx就会冲突,否则就不会冲突。
在inx=10的时候,id<=inx就会冲突,否则就不会冲突。
inx在范围(2, 10)的时候不管怎么样都会冲突。

这样看来,当对索引加范围锁时,主键更像是一个二级索引值。主位是索引,副位是主键,或者叫小数偏移量,总结公式:

inx_offset=inx+(id-inx)/max_num

这个max_num可以理解为主键id能取到的最大值,但不是代表当前的最大值。
当索引inx=2时,这个insert是否落在(2, 10]范围内,要看主键,当主键=2的时候,这个值inx_offset=2+(2-2)/max_num=2,也就是落在了(2, 10]外面,也就是没有冲突。
但是当主键=3或更大的时候,值就变成inx_offset=2+(3-2)/max_num=2.*>2,。那么这个数就比2大了,就落入了(2,10]范围内,就冲突了。
同样,解释索引inx=10的情况,如果id=10,那么inx_offset=10+(10-10)/max_num=10,落在了(2,10]范围内,冲突了;
inx=10,id=11,inx_offset=10+(11-10)/max_num>10,不在(2,10]范围内,不冲突;
inx=10,id=1,inx_offset=10+(1-10)/max_num<10,落在(2,10]范围内,冲突了。
现在通过观察和测试,是这样的结果,欢迎大家指点。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值