前言

本篇主要介绍Mysql的InnoDb引擎RR事务隔离级别下一些锁相关的内容,结合Mysql官方文档和一些演示来说明不同sql与其加锁的方式。Innodb有哪些类型锁呢?锁类型介绍主要偏概念一些;最后再深入看下insert场景的加锁方式,我们容易忽略的地方。

文中介绍与案例基于Mysql 8.0.32版本。


Innodb的锁类型

S and X Locks

S和X是InnoDb的两种标准行级别锁。

  • S:共享锁,允许持有锁的事务进行查询。
  • X:排他锁,允许持有锁的事务进行更新与删除。

互斥关系,除了S与S不互斥,其它关系均互斥。即事务A已经持有某行的S锁时,事务B可以也持有该行的S锁,但是不能获取到其X锁。

其实表级别也有S和X类型锁,互斥关系同上。


Intention Locks

表级别的意向锁,它表示有事务以哪种方式(S/X锁)进行锁行。
IS:意向共享锁:一个事务想要设置S锁在表的行上。
IX:意向排他锁:一个事务想要设置X锁在表的行上。

表锁和意向锁的互斥关系,如下所示:
image-1698066418159

意向锁只会阻塞表级别锁请求,如 lock tables…write;意向锁不会阻塞任何其它类型的行级别锁


Record Locks

记录锁是锁在索引记录上的,即使表没有创建索引,Innodb创建表时也会创建一个隐藏的聚簇索引。


Gap Locks

间隙锁锁在索引两个记录的间隙间或者从某个开始到某个结束的间隙,为了阻止其它事务进行插入这个间隙数据。RR隔离级别下防止泛读。

间隙锁特点:
1、间隙锁不会加在对一个唯一索引的唯一行搜索(不包括组合唯一索引的前缀搜索),是一种锁优化,不锁间隙。
2、不同事务对相同间隙加间隙锁不互斥。例如在一个间隙,A事务有共享级别的间隙锁(gap S-lock)然后B事务可以有排他级别的间隙锁(gap X-lock)
3、间隙锁只用于Innodb引擎的RR事务隔离级别,READ_COMMITED不生效。


Next-Key Locks

Next-Key锁是一种组合锁(Rocord Locks + Gap Locks),间隙Gap指的是这个index Record的之前。记住前开后闭区间

特点:
1、当进行索引数据扫描的时候,会进行加锁,默认加的是Next-Key锁,锁住索引行和它之前的间隙。
2、索引中有个虚拟的最大值“supremum” pseudo-record,用来表示最大值到无穷大这个间隙。

如果一个索引包括数据10、11、13、20,那么可能的Next-Key:
(negative infinity, 10]
(10, 11]
(11, 13]
(20, positive infinity) 或 (20, supremum]

符号( 表示开期间 不锁终点值,] 表示闭区间 锁终点值


Insert Intention Locks

插入意向锁是一种间隙锁,在执行insert操作前会被设置。表明了插入的意图,如果两个事务插入索引同一个间隙的不同位置,不会相互阻塞。

两个例如:
1、索引数据4 和 7,两个事务分别在其间插入5,6并不会阻塞,因为插入意向锁本身并不互斥

2、如果1个间隙已经被一个事务持有Gap Locks,此时另一个事务是无法插入该间隙的,因为INSERT_INTENTION锁waiting,通过语句 SHOW ENGINE INNODB STATUS 可以看出来:“ lock_mode X locks gap before rec insert intention waiting ”


AUTO-INC Locks

是一种特殊表级别锁。如果表中有自增字段,当一个事务在执行插入操作时,会被其它AUTO_INC锁所阻塞,但是前提是innodb_autoinc_lock_mode不等于2,mysql 8.0之后默认为2表示id生成方式为混合方式,所以8.0后不会加该锁,牺牲连续来获得性能和并发。

更多信息可以参考>


不同的sql对应不同的锁

带锁读(for update / lock in share mode)、update、delete语句都会导致锁索引,不管结果是否会返回,只要扫描了该索引行,通常便会加 Next-Key锁。(唯一索引搜索到唯一值则降级成 Record锁)。

如果没有合适的索引,会扫描所有的聚簇索引数据,便会给所有的记录加Next-Key锁,相当于锁表

初始化测试表:

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),(2,3,4),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(21,21,21),(25,25,25);

select…from

查询快照,不会加任何锁。

如:
select * from t where id = 2;

select…for update/share or update or delete

加锁取决于是否是唯一索引。
1、如果是唯一索引的等值搜索,只会锁索引行,不会锁间隙
如:select * from t where id = 5 for update;

2、如果普通索引搜索,会对扫描到的索引行加间隙或Next-Key锁。

insert

插入语句会设置一个X类型的Recode锁在插入行,在插入之前会加一种叫插入意向锁的间隙锁。

1、如果仅有1个事务进行插入,不会加行锁,如下:
image-1698159029053


2、间隙锁会导致数据无法插入,等待INSERT_INTENTION
事务1:

begin;
select * from t where id >= 5 and id <= 10 for update;

事务2:

begin;
insert into t values(6,6,6);

持有锁情况:
image-1698160008946

之所以在lock_data等于10上waiting,是事务2要在10之前的间隙加插入间隙锁.
成功插入之后,在id=6这一行加入Record X-lock


3、重复键:
如果发生重复键等待,那么会等待一种共享Record锁,如果超过2个以上等待该锁,那么等到持有X锁的事务释放或回滚,会导致死锁的发生,如下:

依次执行以下事务:

事务1:

begin;
insert into t values(6,6,6);

此时只有IX表级别意向锁。

事务2:

begin;
insert into t values(6,6,6);

此时锁信息:
image-1698159235258

事务3

begin;
insert into t values(6,6,6);

此时锁信息:
image-1698159275737

  • 当对事务1进行commit:
    事务2和事务3均报错:ERROR 1062 (23000): Duplicate entry ‘6’ for key ‘t.PRIMARY’

  • 当对事务1进行rollback:
    事务2和事务3发生死锁:DeadLock,因为他们都等待持有共享锁,共享锁不互斥,都持有共享锁,再去申请排他锁(insert需要),都会因为对方持有共享锁而发生死锁。


总结

Innodb引擎在RR隔离级别下的锁有S/X、意向锁、记录锁、间隙锁、Next-Key锁、插入意向锁、AUTO-INC锁。

其中意向锁(不包括插入意向锁)和AUTO-INC锁属于表级别锁,其它属于行级别锁。

插入语句的加锁会稍微少见些,只需要先记住,间隙锁是为了防止插入语句进行插入间隙,防止幻读。

关于锁的规则较多,记住一些常见的情况,遇见锁等待结合performance_schema.data_locks;表进行分析。


参考:
1、InnoDB Locking
2、Locks Set by Different SQL Statements in InnoDB