掘金 后端 ( ) • 2024-04-09 18:03

theme: juejin

c071cdc46f0c4867a1d52d0cb51fc6d6.jpg

一、InnoDB中的表锁

1. 共享锁与排他锁(读写锁)

  • 首先说明一点,对于 InnoDB 引擎来说,读写锁可以加在表上,也可以加在行上。
  • 读锁,又称共享锁(Share locks,简称 S 锁),加了读锁的记录,所有的事务都可以读取,但是不能修改,并且可同时有多个事务对记录加读锁。
  • 写锁,又称排他锁(Exclusive locks,简称 X 锁),或独占锁,对记录加了排他锁之后,只有拥有该锁的事务可以读取和修改,其他事务都不可以读取和修改,并且同一时间只能有一个事务加写锁。

2. 意向锁

  • 由于表锁和行锁虽然锁定范围不同,但是会相互冲突。所以当你要加表锁时,势必要先遍历该表的所有记录,判断是否加有排他锁。这种遍历检查的方式显然是一种低效的方式,MySQL 引入了意向锁,它的主要目的是表明有人正在锁定一行,或者要锁定表中的一行,可以快速检测表锁和行锁的冲突。
  • 意向锁也是表级锁,也可分为读意向锁(IS 锁)和写意向锁(IX 锁)。当事务要在记录上加上读锁或写锁时,要首先在表上加上意向锁。这样判断表中是否有记录加锁就很简单了,只要看下表上是否有意向锁就行了。
  • 意向锁之间是不会产生冲突的,也不和 AUTO_INC 表锁冲突,它只会阻塞表级读锁或表级写锁,另外,意向锁也不会和行锁冲突,行锁只会和行锁冲突。
  • 注意:只有获取表中的行锁时,才会需要先申请意向锁。对于常见的 DDL 语句(如ALTER、CREATE 等),是不需要申请意向锁的,InnoDB 会自动给相应的表加表级X锁(隐式锁定)。

3.自增锁

AUTO-INC 锁又称为自增锁(简称 AI 锁),是一种特殊的表锁,当表中有自增(AUTOINCREMENT)时出现。当插入数据时,如果表中有自增列,数据库需要自动生成自增值,它会先为该表加AUTOINC表锁,阻塞其他事务的插入操作,这样保证生成的自增值肯定是唯一的。AUTOINC 锁具有如下特点:

  • AUTO_INC 锁互不兼容,也就是说同一张表同时只允许有一个自增锁;
  • 不遵循二段锁协议,它并不是在事务提交时释放,而是在insert语句执行完成之后就释放,提高了并发插入的性能。
  • 自增值一旦分配了就会 +1,如果事务回滚,自增值也不会减回去,所以自增值可能会出现中断的情况。

显然,AUTOINC 表锁会导致并发插入的效率降低,为了提高插入的并发性,MySQL 从 5.1.22 版本开始,引入了一种可选的轻量级锁(mutex)机制来代替 AUTOINC 锁,可以通过参数 innodb_autoinc_lock_mode 来灵活控制分配自增值时的并发策略。具体可以参考官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-auto-increment-handling.html

4.各个表锁之间的兼容性

image.png
总结起来有下面几点:

  • 意向锁之间互不冲突;
  • 上表中的读写锁指的是表级锁,意向锁不会与行级的读写锁互斥;
  • S 锁只和 S/IS 锁兼容,和其他锁都冲突;
  • X 锁和其他所有锁都冲突;
  • AI 锁只和意向锁兼容;

二、InnoDB中的行锁(加在索引上的)

1.记录锁

记录锁是对索引记录的锁定,换句话说就是,记录锁只会锁定索引。每一个表必定会有一个主键索引(用户定义的主键、唯一索引、隐式生成),而该主键索引中的非叶子节点中的记录就是使用该记录锁进行锁定。
除了主键索引之外,InnoDB 中还会有二级索引。二级索引跟主键索引一样,在使用二级索引作为查询条件时,会将符合条件的二级索引的记录使用记录锁进行锁定,然后再回表将对应的主键索引也使用记录锁进行锁定。

2.间隙锁

间隙锁(简称为 Gap)是在索引记录之间的间隙上的锁,或在第一条索引记录之前或最后一条索引记录之后的间隙上的锁(因为每个表中都会存在两个隐式记录:最小记录infimum,最大记录supermum)。这个间隙可以跨一个索引记录,多个索引记录,甚至是空的。间隙锁在 RR 隔离级才有,使用间隙锁可以防止其他事务在这个范围内插入或修改记录,保证两次读取这个范围内的记录不会变,从而不会出现幻读现象。
使用唯一索引(或者主键索引)进行等值比较获取一条存在的索引记录时是没有间隙锁的。这是因为唯一索引进行等值比较只能获取一条记录,不会出现多条记录的情况,那么也就不会出现多次读取出现不一致的情况。但是当搜索条件仅包括多列唯一索引的某些列的时候或者查询的是主键上不存在的记录时也还是会可能发生间隙锁定的。
间隙锁的主要目的是阻止事务往间隙中插入记录,并且间隙锁之间是可以共存的,多个事务可以同时获取得到相同间隙的锁。共享间隙锁和排他间隙锁之间并没有区别,它们是完全一样的东西。

3.Next-Key锁

Next-key锁是记录锁和间隙锁的组合,它指的是加在某条记录以及这条记录前面间隙上的锁:
image.png
假设一个age索引包含15、18、20 ,30,49,50 这几个值,可能的 Next-key 锁如下:

(-∞,15],(15, 18],(18, 20],(20, 30],(30, 49],(49, 50],(50, +∞)

通常我们都用这种左开右闭区间来表示 Next-key 锁,其中,圆括号表示不包含该记录,方括号表示包含该记录。对于最后一个间隔,next-key lock 锁定索引中最大值上方的间隙,并且“ supremum ” 伪记录的值高于索引中的任何实际值。上界不是真正的索引记录,因此,实际上,这个下一个键锁只锁定最大索引值之后的间隙。
和间隙锁一样,在 RC 隔离级别下没有 Next-key 锁,只有 RR 隔离级别才有。
如果上面的索引不是主键,而是二级索引,且不是唯一索引,那么select * from xxx where age=49在 RR 隔离级别下就会加如下的 Next-key 锁 (30, 49](49, 50)此时如果插入一条 id = 31 的记录将会阻塞住。之所以要把 id = 49 前后的间隙都锁住,仍然是为了解决幻读问题,因为 id 是非唯一索引,所以 id = 49 可能会有多条记录,为了防止再插入一条 id = 49 的记录。
当 SQL 语句无法使用索引时,会进行全表扫描,这个时候 MySQL 会给整张表的所有数据行加Next-key锁。
特别注意一点,辅助索引值相同,主键值是按升序排列的。比如索引49对应的主键id是10,如果我插入一条索引是49对应主键id是8的记录那么这条记录就会插入到(30,49)这个间隙,如果我再插入一条索引是49对应主键id是12的记录那么这条记录就会插入(49,50)这个间隙。

4.插入意向锁

插入意向锁是一种特殊的间隙锁(简写成 II GAP)表示插入的意向,只有在 INSERT 的时候才会有这个锁。注意,这个锁虽然也叫意向锁,但是和上面介绍的表级意向锁是两个完全不同的概念,不要搞混了。
插入意向锁和插入意向锁之间互不冲突,所以可以在同一个间隙中有多个事务同时插入不同索引的记录。譬如在上面的例子中,age = 30 和 age = 49 之间如果有两个事务要同时分别插入 id = 32 和 id = 33 是没问题的,虽然两个事务都会在age = 30 和 age = 49之间加上插入意向锁,但是不会冲突。
插入意向锁只会和间隙锁或 Next-key 锁冲突,正如上面所说,间隙锁唯一的作用就是防止其他事务插入记录造成幻读,正是由于在执行 INSERT 语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。

5.InnoDB四种行锁的兼容性

image.png
其中,第一行表示已有的锁,第一列表示要加的锁。插入意向锁较为特殊,所以我们先对插入意向锁做个总结,如下:

  • 插入意向锁不影响其他事务加其他任何锁。也就是说,一个事务已经获取了插入意向锁,对其他事务是没有任何影响的;
  • 插入意向锁与间隙锁和 Next-key 锁冲突。也就是说,一个事务想要获取插入意向锁,如果有其他事务已经加了间隙锁或 Next-key 锁,则会阻塞。

其他类型的锁的规则较为简单:

  • 间隙锁不和其他锁(不包括插入意向锁)冲突;
  • 记录锁和记录锁冲突,Next-key 锁和 Next-key 锁冲突,记录锁和 Next-key 锁冲突;