目录
- 一、序言
- 二、各种锁详细介绍
- 1、Shared and Exclusive Locks(共享锁和读占锁)
- 2、Intention Locks(意向锁)
- 3、Record Locks(记录锁)
- 4、Gap Locks(间隙锁)
- 5、Next Key Locks (Next Key锁)
- 6、Insert Intention Locks (插入意向锁)
- 7、AUTO-INC Locks (自增锁)
- 8、Predicate Locks for Spatial Indexes (空间索引Predicate锁)
- 三、Server层的MDL(Metadata Lock)锁
- 四、总结
一、序言
在MySQL中大部分存储引擎用的都是InnoDB
,因此我们具体聊聊InnoDB中支持的锁类型,本章节中的内容会根据mysql 8.0版本官方文档翻译进行阐述。
InnoDB
中锁的主要类型,如下:
- Shared and Exclusive Locks:共享锁和独占锁。
- Intention Locks:意向锁。
- Record Locks:记录锁。
- Gap Locks:间隙锁。
- Next-Key Locks:Next-Key锁。
- Insert Intention Locks:插入意向锁。
- AUTO-INC Locks:自增锁。
- Predicate Locks for Spatial Indexes:用于空间索引的Predicate锁。
二、各种锁详细介绍
1、Shared and Exclusive Locks(共享锁和读占锁)
InnoDB实现了标准级别的锁,这种锁主要有两种类型,共享锁(S)
和独占锁(X)
。
共享锁(S)
允许获得锁的事务读行数据。独占锁(X)
允许获得锁的事务去更新或者删除行。
如果事务T1已经获取了某行数据R的共享锁,然后事务T2针对这行数据R获取锁的请求将如下处理:
- 事务T2如果请求获取共享锁(S),那么将会授权成功。T1和T2两个事务都会持有对数据行R的共享锁(S)。
- 事务T2如果请求获取独占锁(X),那么授权将会失败,事务T2必须等待T1释放共享锁(S)。
备注:共享锁和读占锁只是一种概念,表级别和行级别都有共享锁和独占锁。
2、Intention Locks(意向锁)
InnoDB支持多粒度锁,可以让表锁和行锁共存,这种机制通过意向锁来实现,所谓的意向锁其实是表级别的锁,用来指定事务对某数据行加共享锁或独占锁。意向锁主要有两种类型:
- 意向共享锁(IS):事务倾向于对表中的行加共享锁,在事务对表中的某行获取共享锁之前,必须获取IS锁。
- 意向独占锁(IX):事务倾向于对表中的行加独占锁,在事务对表中的某行获取独占锁之前,必须获取IX锁。
意向加锁示例如下:
加IS锁:
SELECT ... FOR SHARE
加IX锁:SELECT ... FOR UPDATE
表级别锁类型兼容性总结如下表:
意向锁设计的主要目的是为了表明事务正在锁定某行或者将要锁定某行。
3、Record Locks(记录锁)
所谓记录锁是针对唯一索引记录的锁,如果c1是唯一索引列,执行SQL:SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
。其它事务不能对c1等于10的记录进行修改、删除、新增。
记录锁总是会锁定索引记录,即使表本身没有索引,InnoDB也会创建隐藏的聚簇索引用于记录的锁定。
4、Gap Locks(间隙锁)
间隙锁用来锁定索引记录之间的间隙,第一条索引记录前或者最后一条索引记录的后面。间隙可以跨越单个索引值、多个索引值,甚至是空值。
这句话可能不好理解,举个例子,表t01上有id和value两列,id为主键,value为索引列。
create table t01
(
id bigint primary key auto_increment comment '主键',
value bigint not null comment '值'
) comment '测试表t01';
create index idx_t01_value on t01 (value);
insert into lock_test.t01 (id, value)
values (1, 1),
(2, 2),
(10, 5),
(11, 9),
(15, 10),
(25, 25);
事务T1执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t01 where value between 5 and 10 for update;
+----+-------+
| id | value |
+----+-------+
| 10 | 5 |
| 11 | 9 |
| 15 | 10 |
+----+-------+
3 rows in set (4.30 sec)
上面的语句针对value列会产生 (2 , 5), (5 , 10), (10, 25) 3个间隙,也就是说value在[2 , 25)
之间的插入都会失效。
t01表中数据如下:
事务T2执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t01(id, value) values (26,5);
^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> insert into t01(id, value) values (26,2);
^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> insert into t01(id, value) values (15,1);
^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
间隙锁其实是性能和并发度之间权衡,它只针对隔离级别为可重复读
才生效,其它隔离级别是不会生效的。
间隙锁锁定行记录锁时并不是根据唯一索引去搜寻记录,如果表中的某列为唯一索引,那么此时不会上间歇锁,而是使用上面提到的记录锁。
假设id为主键或者唯一索引,那么下面这条SQL只会锁定id为100的记录。
SELECT * FROM child WHERE id = 100;
5、Next Key Locks (Next Key锁)
所谓的Next Key锁其实就是记录锁和间隙锁(只锁定索引记录前)的结合,InnoDB在对数据行加锁时会扫描表中的索引,并且对索引记录加共享锁或者独占锁。
咱们还是拿t01表中的数据举例:
事务T1执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t01 where value = 9 for update ;
+----+-------+
| id | value |
+----+-------+
| 11 | 9 |
+----+-------+
1 row in set (0.00 sec)
上面的语句将会产生行记录锁,锁定value为9的记录,还会产生间隙锁,锁定间隙[5 , 9)
之间的记录(即上面说的索引记录9之前的一个间隙),也就是说插入value为[5, 9]
的记录都会失败。
事务T2执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t01(id, value) values (26,8);
^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t01(id, value) values (3,25);
^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> insert into t01(id, value) values (26,5);
^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
两个的组合也就是所谓的Next Key锁,这里的间隙锁只锁定了value为5到9之间的记录,因为查询条件是等值查询
。
不同查询条件对间隙锁的锁定范围也会有影响,依然用上面t0表的数据为例:
between 5 and 10
之类的范围查询会让间隙锁范围更大,不止锁定索引记录前,而且会锁定索引记录后的记录,产生了 (2 , 5), (5 , 10), (10, 25) 3个间隙。where value > 5 and value < 10
的范围查询只会产生 (5, 10) 这个间隙。
6、Insert Intention Locks (插入意向锁)
插入意向锁其实是由INSERT
操作在行插入之前设置的一种间隙锁,当多个事务插入记录到相同索引间隙时彼此并不需要互相等待,除非插入到间隙里相同的位置。
这句话似乎晦涩难懂,咱们还是用t01表中的数据举例:
事务T1执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t01 where value > 10 for update;
+----+-------+
| id | value |
+----+-------+
| 25 | 25 |
+----+-------+
1 row in set (0.00 sec)
mysql> rollback;
上面这条语句会对value 大于10的记录加上间隙锁。
事务T2执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> insert into t01(id, value) values (26,11);
Query OK, 1 row affected (21.57 sec)
事务T3执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t01(id, value) values (27,12);
Query OK, 1 row affected (6.00 sec)
当事务T1未结束之前,事务T2和T3的插入操作都会等待,当事务T1结束之后,事务T2和事务T3虽然插入的value值都在间隙锁定的范围内,但都会执行成功,T2和T3之间不需要互相等待。这就是所谓的插入意向锁的概念。
但如果事务T2和T3插入的是同一个位置,比如T2也执行如下SQL,那么事务T2会挂起。
insert into t01(id, value) values (26,11);
7、AUTO-INC Locks (自增锁)
自增锁是一种特殊的表级别的锁,当事务插入数据到自增列时就会触发。当某个事务在往表中插入数据时,另外一个事务必须等待上个事务执行完毕,实现主键的连续自增。
8、Predicate Locks for Spatial Indexes (空间索引Predicate锁)
InnoDB本身支持空间索引(为数据类型为空间类型的列设置的索引),涉及到空间索引相关的锁操作,使用Next-Key锁无法支持可重复读
和序列化
这两个隔离级别,因为对于多维的数据是没有绝对排序的概念。
为了对有空间索引的表也支持上述的可重复读和序列化隔离级别,InnoDB会用Predicate
锁来进行处理。
备注:关于空间索引和空间数据类型大家可以去官方文档上看详细描述,这里不再赘述。
三、Server层的MDL(Metadata Lock)锁
上面概述的锁都是基于存储引擎InnoDB的,在MySQL Server层也有锁,我们称为MDL(Metadata Lock)锁。
元数据锁不依赖任何存储引擎。此锁不需要显示调用,只要有事务在执行,对应的连接就会取得元数据。当事务执行的时候理论上是不能容忍表结构在中途发生改变。
- MDL锁分为读锁和写锁。读锁和写锁互斥;
- Select和DML语句申请读锁。MDL读锁之间不冲突,所以多个Select和DML语句可以同时执行。
- DDL语句申请写锁,获取写锁时需要等待读锁释放,且申请写锁后会阻塞后续所有MDL锁的获取。
备注:update语句是读锁,因为它修改的是表的数据,而不是结构。
举个例子,事务T1开始执行如下SQL:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t01;
+----+-------+
| id | value |
+----+-------+
| 1 | 2 |
| 2 | 3 |
| 7 | 8 |
+----+-------+
3 rows in set (0.00 sec)
事务T2开始执行如下SQL:
mysql> drop table t01;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql> alter table t01 add index `index_value` (`value` ASC);
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
结论:当开启事务时,事务T1中select语句会获取MDL读锁,而T2中的drop和alter等DDL语句会申请MDL写锁,由于读写锁互斥,所以T2中的DDL操作都会失败。
四、总结
上述我们对锁的类型做了一个详细的阐述,无论是基于存储引擎的还是基于Server的,这里我们再归类一下:
- 根据功能划分:分为共享锁(S)和独占锁(X),S锁和X锁互斥。
- 根据粒度划分:分为表级锁和行级锁,表级锁有
意向锁(Intention Lock)
、自增锁(AUTO-Inc)
。行级锁有记录锁(Record Locks)
,间隙锁(Gap Locks)
、Next-Key锁
、插入意向锁(Insert Intention Locks)
。 - 根据实现划分:分为Server层实现的
元数据锁(Metadata Lock)
和存储引擎
实现的锁。