锁是数据库系统区别于文件系统关键特性。通过锁机制,可以让数据库系统在并发环境中保持数据的一致性,而加锁规则是Innodb存储引擎中较为复杂的部分,本文对innodb的锁类型与加锁规则进行一个简单的梳理。

锁类型

先看下MySQL官方对innodb锁的分类。

分别有

  • Shared and Exclusive Locks 共享/独占锁
  • Intention Locks 意向锁
  • Record Locks 行锁
  • Gap Locks 间隙锁
  • Next-Key Locks 临键锁
  • Insert Intention Locks 插入意向锁
  • AUTO-INC Locks 自增锁
  • Predicate Locks for Spatial Indexes 谓词锁

下面我们逐一分析下。

Shared and Exclusive Locks 共享和独占锁

Innodb实现了两种标准的行级锁。

  • 共享锁
  • 独占锁

共享锁也称为SLock,事务在读取数据是需要获取。

独占锁也称为XLock,事务在修改或者删除的时候需要获取。

这个设计与读写锁是类似的,如果一个事务获取到ROW1的共享锁,那么其他事务也是可以继续获取ROW1的共享锁的,这个过程称为锁兼容(Lock Compatible)

当然了,也存在锁不兼容的情况,比如事务获取到共享锁,其他事务就无法在这个行上面获取排他锁了。

锁兼容情况如下:

类型X LockS Lock
X Lock不兼容不兼容
S Lock不兼容兼容

Intention Locks 意向锁

Innodb存储引擎支持多粒度的锁定,比如行锁,表锁。为了方便处理多粒度的锁定操作,Innodb设计了意向锁的概念。

简单来说,意向锁是一种不与行级锁冲突的特殊表级锁,它的作用是描述是否有事务在对表或者行进行锁操作。

意向锁是由存储引擎维护的,用户无法直接获取/释放意向锁。

加锁顺序

获取任何粒度的锁之前都需要先获取到表的意向锁。

可以将表结构想像成一个树形结构,最顶层是表,中间是页,最底层是行。

如果要获取行锁,需要先获取最顶层表的意向锁。如果获取意向锁失败了,那么也无法获取到行锁。

分类

意向锁同样分为:

  • 意向共享锁 IS Lock
  • 意向独占锁 XS Lock

IS场景:事务想要获取某几行的共享锁,会先获取到IS Lock。

XS场景:事务想要获取到某几行的独占锁,会先获取到XS Lock。

锁兼容性

类型ISIXSX
IS兼容兼容兼容不兼容
IX兼容兼容不兼容不兼容
S兼容不兼容兼容不兼容
X不兼容不兼容不兼容不兼容

可以看到,意向锁之间都是兼容的,不兼容(互斥)的情况仅存在于意向锁与普通锁之间。

另外意向锁不会与行级别的X锁互斥,只会与表级别的X锁互斥。

比如事务A调用

这个语句会对当前tab表id=1的行获取X锁和表的IX锁。

这个时候事务B调用

会先获取tab表的IX锁,此时事务A的IX锁与事务B的X锁不兼容,所以会被阻塞。

刚才那个过程就可以体现意向锁的价值了,事务B不需要查找每个行记录看是否加上X锁,只需要看表上是否有IX锁即可确定自己能不能获取锁。

Record Locks 行锁

Innodb存储引擎中每条记录都是按照行进行存储的,每一条记录上获取的锁称为行锁。

比如事务中调用当前读语句,则可能会对要查询的行添加行锁。

这个语句中,如果c1是主键的话会加上行锁,如果c1是普通索引的话,会加上next-key lock。

加锁顺序和规则后面我们详细的讲解。

使用SHOW ENGINE INNODB STATUS可以查看Innodb的行锁添加状态。

Gap Locks 间隙锁

间隙锁是指对行记录之间的间隙进行加锁,防止间隙中间插入新的数据,造成不一致的问题。

比如事务中调用

当前读的语义是获取到最新的c1 10-20之间的记录,所以事务会对 c1的这个区间内加锁,防止其他事务对这个区间写入数据。

锁兼容性

间隙锁与上面提到的共享独占锁兼容性不同,间隙锁之间不会冲突。
举个例子:
事务A执行

事务B再执行

事务B不会被锁住,即使事务A,B对同一个间隙加锁。

事务隔离级别

间隙锁仅存在于RR的隔离级别中,其他隔离级别是没有间隙锁的。
所以在RC,RU这两个模式下没有办法解决幻读问题、

Next-Key Locks 临键锁

Next-Key Locks可以理解为Gap Lock + Record Lock,既锁定范围,同时锁定记录本身。

Innodb对于行的操作都是采用这种Next-Key Lock。

例如一个索引有10, 11, 13, 20 这四个值,那么Next-Key Lock区间为

共四个区间,一定都是左开右闭的。

Next-Key Locks是Innodb对行操作默认的加锁规则,至于其他的行锁和间隙锁,也是有Next-Key Locks按照一定的规则退化后产生的。

关于加锁规则,文后会详细介绍。

Insert Intention Locks 插入意向锁

插入意向锁是一种Gap锁,不是意向锁。在insert的时候产生。并且当多个插入意向锁在同一个索引间隙内,只要行记录不同,就不会互相锁住。

在insert完成后,会产生行锁。

加锁规则

加锁规则与事务隔离模式有关,每个隔离模式解决的问题不一样,下面我们从RR这一默认的隔离模式说起。

RR通过Next-Key Locks的方式解决了SELECT FOR UPDATE的幻读问题,并且Next-Key Locks是默认的锁形式。

RR模式的加锁规则,总结一下几点:

  • Next-Key Locks为加锁基本单位。行锁,间隙锁均由Next-Key Locks按照一定的规则退化产生。
  • 查找过程中访问到的对象才加锁,如果未命中索引走了全表扫描,那么会对全表加上Next-Key Locks。或者说如果使用覆盖索引而没有使用主键索引,那么主键索引是没有任何锁的。

Next-Key Locks加锁后退化规则:

场景退化规则
唯一索引等值查询, 匹配到行退化成Record Lock
唯一索引范围匹配查询退化成Record Lock+Gap Lock,左闭右开区间
索引等值查询, 未匹配到行向右遍历至第一个不满足条件的值的这个区间会退化成Gap Locks。

加锁实例分析

下面我结合一些具体的例子演示下RR模式下加锁过程。

先创建一个表z

这里表z中a是主键,b是一个普通索引。

唯一索引下Next-Key退化成间隙锁

序号事务A事务B
1select * from z where a = 7 for update;
2insert into z values (7,7); (阻塞)
3insert into z values (9,9); (阻塞)
4update z set b=b+1 where a=10; (OK)
5update z set b=b+1 where a=5; (OK)

我们来分析下加锁流程,首先事务A使用当前读会产生Next-Key Locks,根据查找范围上锁区间为(5,10] (上文左闭右开规则)。

事务B插入7和9均阻塞可以验证Next-Key Locks起了作用。

但是由于a是唯一索引,所以按照退化规则会变成Gap Lock,即把a=10的行锁去掉。我们update a=10可以成功执行证明这个行上没有上锁。

update a=5操作没有阻塞证明5这一行上没有上锁。

非唯一索引下等值查询

序号事务A事务B
1select * from z where b = 5 for update;
2insert into z values (2,2); (阻塞)
3insert into z values (9,9); (阻塞)
4update z set b=b+1 where a=10; (OK)
5update z set b=b+1 where a=5; (阻塞)

我们来分析下这个加锁规则,首先使用for update在索引b上加next-key locks。区间是(0,5]。

b是普通索引,会继续向后搜索并加锁,直到搜索到b=10停下。

根据优化规则b=10的这一行不会加锁。

所以总共锁定区间为(0,10)。

唯一索引下等值查询与范围查询

第一个案例中我们使用等值查询在主键上查找一个不存在的值会产生间隙锁。

下面我们看下查找一个存在的值会如何。

序号事务A事务B
1select * from z where a=5 for update;
2insert into z values (2,2); (OK)
3insert into z values (9,9); (OK)
4update z set b=b+1 where a=10; (OK)
5update z set b=b+1 where a=5; (阻塞)

从结果上可以看到,事务A加的是行锁,影响a=5这一行。

从主键索引开始查找,找到a=5这一行后,满足退化规则,所以naxt-key lock退化为行锁。

范围查找
序号事务A事务B
1select * from z where a>4 and a<6 for update;
2insert into z values (2,2); (阻塞)
3insert into z values (9,9); (阻塞)
4update z set b=b+1 where a=10; (OK)
5update z set b=b+1 where a=5; (阻塞)

事务A的查找结果和上面的等值查询一致,但是查找过程却不一致。这个查找过程会从a=0一直遍历到a=5。所以会先加上(0,5]的next-key lock。

之后继续向右遍历到第一个小于6的值,这里会查找到10。所以继续加上next-key lock。

根据优化规则,去除a=10上的锁,即next-key lock退化成间隙锁。

非唯一索引下的范围查找

序号事务A事务B
1select * from z where a>4 and a<6 for update;
2insert into z values (2,2); (阻塞)
3insert into z values (9,9); (阻塞)
4insert into z values (11,11); (ok)
5update z set b=b+1 where a=10; (阻塞)
6update z set b=b+1 where a=5; (阻塞)

可以看到a=10这一行也被阻塞了,原因是使用的非唯一索引,所以next-key lock无法退化成间隙锁。

其他和上面使用唯一索引范围查找的加锁规则类似,先查找到a=5这一行,会加上(0,5]的next-key lock, 后继续查找直到a=10,继续加上(5,10]的next-key lock。

最后

Innodb的加锁规则还是很复杂的,后面我遇到问题也会继续补充到这里。欢迎大家提出问题一起讨论 🙂

TIPS:

隔离模式的查看和设置

关于隔离模式的查看,Mysql 8及以上版本可以使用

Mysql 8以下版本可以使用

使用

可以调整事务隔离模式

查看innodb锁状态

查看锁状态

参考资料

《MySQL手册》https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html

《MySQL技术内幕(Innodb存储引擎)》

发表评论

邮箱地址不会被公开。 必填项已用*标注