1.锁的种类

InnoDB有三种行锁的算法

Record Lock

总是会去锁住索引记录, 如果表没有设置索引, 引擎会使用隐式的主键来进行锁定

Gap Lock 锁定一个范围, 不包含自身

Next-Key Lock: Gap Lock+Record Lock 范围+自身, 解决幻读问题,前开后闭

previous-key locking:前闭后开

2.加锁规则

前提:RR隔离级别,版本:版本:5.x 系列 <=5.7.24,8.x系统<=8.0.13

原则1:加锁的基本单位是 next-key lock。next-key lock 是前开后闭区间。

原则2:查找过程中访问到的对象才会加锁。

优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。

优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。

一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值(包含)为止。(8.0.25已经修复)

注意:

  • 非唯一索引,需要向右遍历到第一个不符合条件的值才能停止
  • 范围查找就往后继续找
  • 执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁
  • 冲突的锁可以被不同事务在一个间隙上持有(conflicting locks can be held on a gap by different transactions)
  • 读提交隔离级别的优化:在读提交隔离级别下有一个优化,即:语句执行过程中加上的行锁,在语句执行完成后,就要把“不满足条件的行”上的行锁直接释放了,不需要等到事务提交。也就是说,读提交隔离级别下,锁的范围更小,锁的时间更短,这也是不少业务都默认使用读提交隔离级别的原因。

3.案例

示例数据

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);

案例一:等值查询间隙锁

t1

t2

t3

update t set d=d+1 where id=7;

insert into t values (8,8,8);

阻塞

update t set d=d+1 where id=10;

成功

mysql加表锁语句 mysql 加锁_java

分析

t中没有id=7的记录,根据加锁的规则:

1. 根据 原则1,加锁单位是 next-key lock,session A 加锁范围就是 (5,10]

2. 同时根据优化 2,这是一个等值查询 (id=7),而 id=10 不满足查询条件,next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)

所以事务2要往这个间隙里面插入 id=8 的记录会被锁住,但是事务3修改 id=10 这行是可以的

案例二:非唯一索引等值锁

t1

t2

t3

select id

from t

where c = 5 lock in share mode

update t set d=d+1 where id=5

insert into t values (7,7,7)

阻塞

mysql加表锁语句 mysql 加锁_mysql_02

分析

t1给索引 c 上 c=5 的这一行加上读锁

1.根据原则 1,加锁单位是 next-key lock,因此会给 (0,5] 加上 next-key lock

2.因为c不是唯一索引,所以需要向下遍历到第一个不符合条件的值才能停止。因此访问 c=5 这一条记录不能马上停下来,需要向右遍历,查到 c=10 才放弃。根据原则 2,访问到的都要加锁,因此要给 (5,10]加 next-key lock

3.根据优化 2:等值判断,向右遍历,最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。

4.根据原则 2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,因此 t2的 update 语句可以执行完成

访问到的对象才会加锁,这个“对象”指的是列,不是 记录行。 加锁,是加在索引上的。 列上,有索引,就加在索引上; 列上,没有索引,就加在主键索引上

t3要插入一个 (7,7,7) 的记录,就会被 t1 的间隙锁 (5,10) 锁住

案例三:主键索引范围锁

t1

t2

t3

select * from t where id>=10 and id

insert into t values (8,8,8);

OK

insert into t values (13,13,13)

阻塞

update t set d=d+1 where id=15

阻塞

mysql加表锁语句 mysql 加锁_数据库_03

分析

1.开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10]。 根据优化 1, 主键 id(唯一索引) 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁

2.范围查找就往后继续找,找到 id=15 这一行停下来,因此需要加 next-key lock(10,15]

所以,t1 锁的范围就是主键索引上,行锁 id=10 和 next-key lock(10,15]

案例四:非唯一索引范围锁

t1

t2

t3

select * from t where c>=10 and c

insert into t values (8,8,8)

阻塞

update t set d=d+1 where c=15

阻塞

分析

在第一次用 c=10 定位记录的时候,索引 c 上加了 (5,10] 这个 next-key lock 后,由于索引 c 是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终 t1 加的锁是 索引 c 上的 (5,10] 和 (10,15] 这两个 next-key lock

案例五:唯一索引范围锁 bug

t1

t2

t3

select * from t where id>10 and id

update t set d=d+1 where id=20

阻塞

insert into t values (16,16,16)

阻塞

t1 是一个范围查询,按照原则 1 的话,应该是索引 id 上只加 (10,15]这个 next-key lock,并且因为 id 是唯一键,所以循环判断到 id=15 这一行就应该停止了,但是实现上,InnoDB 会往后扫描到第一个不满足条件的行为止,也就是 id=20。而且由于这是个范围扫描,因此索引 id 上的 (15,20]这个 next-key lock 也会被锁上

这里锁住 id=20 这一行的行为,其实是没有必要的。因为扫描到 id=15,就可以确定不用往后再找了。但实现上还是这么做了,因此这可以认为是个 bug

案例六:非唯一索引上存在"等值"的例子

向表中再插入一条记录

insert into t values (30,10,30);

此时索引c如下图所示

mysql加表锁语句 mysql 加锁_mysql_04

t1

t2

t3

delete from t where c=10

insert into t values (12,12,12)

阻塞

update t set d=d+1 where c=15

mysql加表锁语句 mysql 加锁_mysql_05

分析

1. t1在遍历的时候,先访问第一个 c=10 的记录。同样地,根据原则 1,这里加的是 (c=5,id=5) 到 (c=10,id=10) 这个 next-key lock

2. t1向右查找,直到碰到 (c=15,id=15) 这一行,循环才结束。根据优化 2,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成 (c=10,id=10) 到 (c=15,id=15) 的间隙锁

因此,delete 语句在索引 c 上的加锁范围,就是下图中蓝色区域覆盖的部分

mysql加表锁语句 mysql 加锁_mysql_06

案例七:limit 语句加锁

t1

t2

delete from t where c=10 limit 2

insert into t values (12,12,13)

OK

分析

t1 的 delete 语句加了 limit 2。表 t 里 c=10 的记录有两条,因此加不加 limit 2,删除的效果都是一样的,但是加锁的效果却不同。可以看到,t2 的 insert 语句执行通过了,跟案例六的结果不同

delete 语句明确加了 limit 2 的限制,因此在遍历到 (c=10, id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了

因此,索引 c 上的加锁范围就变成了从(c=5,id=5) 到(c=10,id=30) 这个前开后闭区间,(c=10,id=30)之后的这个间隙并没有在加锁范围里,因此 insert 语句插入 c=12 是可以执行成功的。如下图所示:

mysql加表锁语句 mysql 加锁_java_07

案例八:死锁

t1

t2

select id from t where c=10 lock in share mode;

update t set d=d+1 where c=10

阻塞

insert into t values (8,8,8);

[40001][1213] Deadlock found when trying to get lock; try restarting transaction

分析

1. t1 启动事务后执行查询语句加 lock in share mode,在索引 c 上加了 next-key lock(5,10] 和间隙锁 (10,15);

2. t2 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待。

先加(5, 10)的间隙锁,然后加10的行锁,锁住,还没有来得及加(10,15]的next-key lock呢,就被10的行锁给锁住了,所以这个时候t1如果插入(12,12,12)是不会被session B的间隙锁给锁住。

3. 然后 t1 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁,InnoDB 让 session B 回滚

t2 中 “加 next-key lock(5,10] ” 操作,实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 c=10 的行锁,这时候才被锁住的。

总结

在分析加锁规则的时候可以用 next-key lock 来分析。具体执行的时候,是要分成间隙锁和行锁两段来执行的

在删除数据的时候尽量加 limit。这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围