MySQL中的InnoDB行锁

MySQL InnoDB Record lock、Gap Lock and Next-Key Lock

Posted by alovn on October 23, 2021

MySQL中InnoDB存储引擎在默认隔离级别(Repeatable Read,可重复读)下有三种行锁的算法:

  • Record Lock: 单个行记录上的锁,记录锁(也称行锁)
  • Gap Lock: 间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock: Gap Lock+Record Lock,锁定一个范围,并且所对应记录本身

记录锁很好理解。关于间隙锁,可以想象一下生活中排队的场景,小张、小李,小赵三个人依次站成一排,此时,如何让新来的小明不能站在小李旁边,那么只要将小李与他前面的小张之间、与后面的小赵之间的空隙封锁,那么在封锁期间小明就不能站到小李的旁边。可以把小张、小李、小赵看作数据库中索引上已存在并且连续的三条记录,而小明就是要新插入的一条记录。

Next-Key Lock则结合了Gap Lock和Record Lock。

假设一个索引有10、11、13、20这几个值,那么这个索引可能会被Next-Key Locking的区间为:

1
2
3
4
5
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)

主键索引

如果查询使用的到主键或唯一索引时,InnoDB存储引擎会对Next-Key Locking进行优化,将它降级为Record Lock,即只锁住索引本身,而不是范围,从而提高并发性。例如:

1
2
3
4
5
6
create table t1(
    id int primary key
) engine = innodb;
insert into t1 select 1;
insert into t1 select 2;
insert into t1 select 5; -- 查询此行

接着按下面来执行SQL:

时间会话1会话2
1begin; 
2select * from t1 where id=5 for update; 
3 begin;
4 insert into t1 select 4;
5 commit;
6commit; 

上面例子中,在会话1中会首先对id=5加X锁,并且由于id是唯一主键,从Next-Key Lock降级为Record Lock,所以只会锁定5这个索引,而不会锁定(2,5)范围,这样会话2中插入id为4的值时就不会阻塞,从而提高并发性。

注意:对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询的唯一索引列。若唯一索引由多个列组成,而仅仅查找多个唯一索引列其中的一个,那么它其实是range类型的查询,InnoDB存储引擎还是会使用Next-Key Lock进行锁定,并不会降级。

若对于表t执行SQL语句 select * from t1 where id > 2 for update,那么锁定的不是5这一个值,而是对(2, +∞)这个范围加了X锁,所以任期其它会话中对于这个范围内的插入都是不被允许的。

辅助索引

若查询使用辅助索引时,就会使用Next-Key Locking加锁,锁定一个范围。若表中有多个索引,还需要分别进行锁定。例如:

1
2
3
4
5
6
7
8
9
10
11
create table t2 (
    id int,
    cid int,
    primary key(id),
    key(cid)
)engine=innodb;
insert into t2 select 1,1;
insert into t2 select 3,1;
insert into t2 select 5,3; -- 查询此行
insert into t2 select 7,6;
insert into t2 select 10,8;

列cid是表t的辅助索引,若执在会话中执行下面的SQL:

1
select * from t2 where cid=3 for update;

这时SQL语句通过辅助索引列cid进行查询,因此会使用Next-Key Locking加锁,并且由于同时有两个索引,则分别进行锁定,对于聚集索引仅对id等于5的索引上加Record Lock。而对于辅助索引,Next-Key Lock锁定的范围为(1,3],需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁。因此,若在新的会话中执行下面的SQL语句,都会被阻塞:

1
2
3
select * from t2 where id = 5 for update;  -- id为5的行已加X锁
insert into t2 select 4,2; -- 辅助索引cid范围(1,3)已被锁定,所以插入2会被阻塞
insert into t2 select 6,5; -- 辅助索引cid范围(3,6)已被锁定,所以插入5会被阻塞

从上面例子可以看出,间隙锁Gap Lock的作用是为了防止多个事务将记录插入到同一范围内,它解决了幻读的问题。幻读是在同一事务下,连续执行两次同样的SQL查询可能会得到不同的结果,第二次查询到的结果可能返回之前不存在的行。上面例子中,会话1已经锁定了cid等于3的记录,并且增加了间隙锁(1,3)和(3,6)。若不存在这个间隙锁,那么就可以插入cid列为3的记录,那么会话1中若再次执行相同的查询就会返回不同的记录,也就是会出现幻读的问题。增加的两个间隙锁正是为了防止再插入cid=3的记录。所以说间隙锁解决了幻读的问题。