最近做了一个插入锁的尝试,感觉很奇怪,这里展示一下并写出自己的分析结果。
先创建一个表格:
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]范围内,冲突了。
现在通过观察和测试,是这样的结果,欢迎大家指点。