mysql lock row lock - yaokun123/php-wiki GitHub Wiki
间隙锁在可重复读隔离级别下才有效
加锁规则
总结为两个“原则”、两个“优化”和一个“bug”
原则1:加锁的基本单位是next-key lock,前开后闭区间;
原则2:查找过程中访问到的对象都会加锁;
优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁;
优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,nextkeylock 退化为间隙锁;
一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止;
以表t为例
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) NOT NULL,
`d` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
一、案例一:等值查询间隙锁
sessionA | sessionB | sessionC |
---|---|---|
begin;update t set d=d+1 where id=7; | ||
insert into t values(8,8,8);(blocked) | ||
update t set d=d+1 where id=10;(Query OK) |
加锁规则判断:
表中没有id=7记录,根据原则1,加锁的单位是next-key lock,sessionA的加锁范围是(5,10];
根据优化2,这是一个等值查询(id=7),id=10不满足查询条件,next-key Lock退化成间隙锁,最终加锁的范围就是(5,10);
二、非唯一索引等值锁
sessionA | sessionB | sessionC |
---|---|---|
begin;select id from t where c=5 lock in share mode; | ||
update t set d=d+1 where id=5;(Query OK) | ||
insert into t values(7,7,7);(blocked) |
加锁规则判断:
根据原则1,加锁单位是next-key lock,因此会给(0,5]加上next-key lock;
c是普通索引,向右遍历查到c=10才放弃,根据原则2,访问到的都要加锁,索引(5,10]加next-key lock;
符合优化2,等值判断,向右遍历,最后一个值不满徐足c=5这个等值条件,退化成间隙锁(5,10);
根据原则2,只有访问到的才加锁,查询采用的覆盖索引,并不需要访问主键索引,主键索引上没有任何锁,所以sessionB的update语句可以执行;
sessionC要插入(7,7,7)就会被sessionA的间隙锁(5,10)挡住
注意:
如果将select id from t where c=5 lock in share mode;换成
select id from t where c=5 for update;
则会强制将主键索引也给锁上,此时sessionB就会变为阻塞
三、案例三:主键索引范围锁
sessionA | sessionB | sessionC |
---|---|---|
begin; select * from t where id>=10 and id<11 for update; | ||
insert into t values(8,8,8);(Query OK)insert into t values(13,13,13);(blocked) | ||
update t set d=d+1 where id=15;(blocked) |
开始执行的时候找到第一个id=10的行,根据原则1,next-key lock(5,10]退化成行锁,只加了id=10这一行的行锁;
范围查找向后继续查找,找到id=15这一行停下来,需要加next-key lock(10,15]。
所以sessionB和sessionC都被阻塞。
四、案例四:非唯一索引范围锁
sessionA | sessionB | sessionC |
---|---|---|
begin; select * from t where c>=10 and c<11 for update; | ||
insert into t values(8,8,8);(blocked) | ||
update t set d=d+1 where c=15;(blocked) |
sessionA的加锁场景,索引c添加了(5,10]和(10,15]这两个next-key lock
所以sessionB和sessionC的操作都被阻塞;
五、案例五:唯一索引范围锁bug
sessionA | sessionB | sessionC |
---|---|---|
begin; select * from t where id>10 and id<=15 for update; | ||
update t set d=d+1 where id=20;(blocked) | ||
insert into t values(16,16,16);(blocked) |
sessionA是一个范围查询,根据原则1,在索引d上只加了(10,15]这个next-key lock,id是唯一键,循环判断到id=15这一行停止;
实现上,InnoDB会往前扫描到第一个不满足条件为止,所以索引id上(15,20]这个next-key lock也会被锁上;
所以sessionB和sessionC的操作都会被锁住;
六、案例六:非唯一索引上存在“等值”的例子
insert into t values(30,10,20);
新插入的这一行c=10,现在表中存在两个c=10的行
虽然有两个c=10,但是主键值id是不同的(分别是10和30),因此两个10之间也是有间隙的 采用delete语句来验证:
sessionA | sessionB | sessionC |
---|---|---|
begin; delete from t where c=10; | ||
insert into t value(12,12,12);(blocked) | ||
update t set d=d+1 where c=15;(Query OK) |
sessionA遍历的时候是(c=5,id=5) 到 (c=10,id=10) 这个 next-key lock,session A 向右查找,直到碰到 (c=15,id=15) 这一行,循环才结束;
根据优化2,等值查询,向右查找到了不满足条件的行,所以会退化成(c=10,id=10) 到 (c=15,id=15) 的间隙锁;
也就是说,这个 delete 语句在索引 c 上的加锁范围,就是下图中蓝色区域覆盖的部分
蓝色区域是虚线,表示开区间,即(c=5,id=5) 和 (c=15,id=15) 这两行上都没有锁;
六、案例七:limit加锁
sessionA | sessionB |
---|---|
begin; delete from t where c=10 limit 2; | |
insert into t value(12,12,12);(Query OK) |
和案例六相比加锁的效果不同,可以看到,session B 的 insert 语句执行通过了,跟案例六的结果不同。
案例七里的 delete 语句明确加了 limit 2 的限制,因此在遍历到 (c=10,id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。
因此索引 c 上的加锁范围就变成了从(c=5,id=5) 到(c=10,id=30) 这个前开后闭区间。
可以看到,(c=10,id=30)之后的这个间隙并没有在加锁范围里,因此 insert 语句插入 c=12 是可以执行成功的。
在删除数据的时候尽量加limit,不仅可以控制删除数据的条数,还可以让数据更加安全,减少加锁的范围。
总结
可重复读隔离级别遵守两阶段锁协议,所有加锁的资源,都是在事务提交或者回滚的时候才释放的;
next-key lock 实际上是由间隙锁加行锁实现的。如果切换到读提交隔离级别 (read-committed) 的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分