一条 select ... for update 会加什么锁?

233 阅读5分钟

select ... for update 语句属于锁定读语句,它会对表的记录加 X 型的行级锁。

不同隔离级别下,行级锁的种类是不同的。

在读已提交隔离级别下,行级锁的种类只有记录锁,也就是仅仅把一条记录锁上。

在可重复读隔离级别下,行级锁的种类除了有记录锁,还有间隙锁(目的是为了避免幻读),所以行级锁的种类主要有三类:

  • Record Lock,记录锁,也就是仅仅把一条记录锁上;
  • Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
  • Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

行级锁加锁规则比较复杂,不同的场景,加锁的形式是不同的。

加锁的对象是索引,加锁的基本单位是 next-key lock,它是由记录锁和间隙锁组合而成的,next-key lock 是前开后闭区间,而间隙锁是前开后开区间。

但是,next-key lock 在一些场景下会退化成记录锁或间隙锁。

那到底是什么场景呢?总结一句,在能使用记录锁或者间隙锁就能避免幻读现象的场景下, next-key lock 就会退化成记录锁或间隙锁。

这次我们只讨论,执行 select ... for update 语句,如果查询条件没有索引字段的话,会加什么锁?

现在假设事务 A 执行了下面这条语句,查询条件中 age 不是索引字段

image.png 这时候有其他事务对这张表进行增删改,都会发生阻塞。

image.png

再来看看,事务  A 持有哪些行级锁?

可以执行 select * from performance_schema.data_locks\G; 这条语句,查看事务 A 此时持有了哪些行级锁。

  • 1 个表级锁:X 类型的意向锁(表级别的锁);
  • 4 个行级锁:X 类型的行级锁;

什么是意向锁?

在 InnoDB 存引擎中,当事务执行锁定读、插入、更新、删除操作后,需要先对表加上「意向锁」,然后再对记录加「行级锁」。

之所以要设计「意向锁」,目的是为了快速判断表里是否有行级锁

意向锁不会和行级锁发生冲突,而且意向锁之间也不会发生冲突,意向锁只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突

所以,当事务 A 执行了查询条件没有索引字段的 select ... for update 语句后,不可能是因为事务 A 持有了意向锁,才导致其他事务无法进行增删改操作

为什么因为事务 A 对表所有记录加了 X 型的 next-key 锁后,其他事务就无法进行增、删、改操作了呢?

其他事务在执行「删除或者更新操作」的时候,也会申请 X 型的  next-key 锁,next-key 锁是包含记录锁和间隙锁的,间隙锁之间虽然是相互兼容的,但是记录锁之间存在 X 型和 S 型的关系,即读读共享、读写互斥、写写互斥的关系。

所以当事务 A 持有了  X 型的 next-key 锁后,其他事务就无法申请 X 型的  next-key 锁,从而发生阻塞。

比如,前面的例子,事务 B 在更新 id = 1 的记录的时候,它会申请 X 型的记录锁(一索引等值操作,  next-key 锁会退化为记录锁),但是因为事务 A 持有了 X 型的 next-key 锁,所以事务 B 在申请 X 型的记录锁的时候,会发生阻塞。

事务 C 的删除操作被阻塞的原因,也是这个原因。

事务 D 的插入操作被阻塞的原因,跟事务 B 和事务 C 的原因不同。

插入语句在插入一条记录之前,需要先定位到该记录在 B+树 的位置,如果插入的位置的下一条记录的索引上有间隙锁,如果已加间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态,现象就是插入语句会被阻塞

事务 D 插入了一条 id = 10 的新记录,在主键索引树上定位到插入的位置,而该位置的下一条记录是 supremum pseudo-record,该记录是一个特殊的记录,用来标识最后一条记录,而该特殊记录上正好持有了间隙锁(next-key 锁包含间隙锁),所以这条插入语句会发生阻塞。

这是因为事务 A 的这条锁定读查询语句,没有使用索引列作为查询条件,所以扫描的方式是全表扫描,行级锁是在遍历索引的时候加上的,并不是针对输出的结果加行级锁

不只是锁定读查询语句不加索引才会导致这种情况,update 和 delete 语句如果查询条件不加索引,那么由于扫描的方式是全表扫描,于是就会对每一条记录的索引上都会加 next-key 锁,这样就相当于锁住的全表。

因此,在线上在执行 update、delete、select ... for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题

如果 select … for update 语句的查询条件没有索引字段的话,整张表都无法进行增删改了,从这个现象看,好像是把表锁起来了,但是并不是因为上面这两个表级锁的原因

而是因为如果锁定读查询语句,没有使用索引列作为查询条件,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁(行级锁),这样就相当于锁住的全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞