加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”。
原则 1:加锁的基本单位是 next-key lock,是前开后闭区间。
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
初始数据:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t
values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
案例一:等值查询间隙锁
Update t set d=d+1 where id = 7;
分析:
使用原则1,由于不存在id=7的记录,所以加锁的是 (5,10】。使用优化2,由于是索引上的等值查询,向右遍历到10 的时候,退化为间隙锁 (5,10)
如果插入记录id=8,则会被lock,而插入id=10,则被允许
案例二:非唯一索引等值锁
Select id from t where c=5 lock in share mode
分析:
使用原则1,加锁单位是next-key lock,所以(0,5】会被加锁。使用原则4,由于是索引是索引上的等值查询,向右遍历到10的时候,退化为间隙锁(5,10);使用原则2,只有访问到的对象才会加锁,而这条查询语句使用的是覆盖索引,并不会访问主键索引,所以主键索引不会被锁住。
在这个例子中,lock in share mode 只锁覆盖索引,但是如果是 for update 就不一样了。
执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。
案例三:主键索引范围锁
mysql> select * from t where id=10 for update;
mysql> select * from t where id>=10 and id<11 for update;
分析:
第一句的加锁范围,使用原则1,加锁范围(5,10】,使用优化1,加锁范围退化为行锁10
第二句的加锁范围
开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10]。根据优化 1, 主键 id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
范围查找就往后继续找,找到 id=15 这一行停下来,因此需要加 next-key lock(10,15]。
所以这时候锁的范围就是主键索引上,行锁 id=10 和 next-key lock(10,15]
案例四:非唯一索引范围锁
mysql> select * from t where c>=10 and c<11 for update;
分析:
由于c是非唯一索引,所以使用优化2,应该是(5 10】(10,15】
案例六:非唯一索引上存在"等值"的例子
接下来的例子,是为了更好地说明“间隙”这个概念。给表 t 插入一条新记录。
mysql> insert into t values(30,10,30);
新插入的这一行 c=10,也就是说现在表里有两个 c=10 的行。
虽然有两个 c=10,但是它们的主键值 id 是不同的(分别是 10 和 30),因此这两个 c=10 的记录之间,也是有间隙的。
Delete from t where c=10
分析这条语句的加锁规则
先访问第一个 c=10 的记录。同样地,根据原则 1,这里加的是 (c=5,id=5) 到 (c=10,id=10) 这个 next-key lock。
向右查找,直到碰到 (c=15,id=15) 这一行,循环才结束。根据优化 2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成 (c=10,id=10) 到 (c=15,id=15) 的间隙锁。
综合起来就是 (c=5,id=5) 到(c=15,id=15),左右都是开区间
案例七:limit 语句加锁
Delete from t where c=10 limit =2
执行结果是一样的,但是加锁规则上,delete 语句明确加了 limit 2 的限制,因此在遍历到 (c=10, id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。
综合起来就是 (c=5,id=5)到 (c=10,id=30),左开右闭,相比上一个案例,少加锁了(c=10,id=30)到(c=15,id=15)之间的间隙。所以delete的时候尽量加上limit,让操作更安全,还可以减小加锁的范围
案例八:一个死锁的例子
目的是说明:next-key lock 实际上是间隙锁和行锁加起来的结果。
Update t set d =d+1 where c=10
update 语句也要在索引 c 上加 next-key lock(5,10],这个锁是分成2步
实际上分成了两步,先是加 (5,10) 的间隙锁,然后加 c=10 的行锁。有可能间隙锁加锁成功,但是行锁加锁失败的情况。再遇到其他加锁冲突的时候,是极有可能造成死锁的