为了提高InnoDB存储引擎的并发性,InnoDB采用了细粒度的行锁,并在针对不同的锁使用不同级别锁或不使用锁。本文将介绍InnoDB中使用的七种锁,分别是共享锁(share,S锁)/排它锁(exclusive,X锁)、意向锁、记录锁、间隙锁、临键锁、插入意向锁、自增锁。
1. 共享锁(S锁)/排它锁(X锁)(Share Lock/Exclusive Lock)
根据字面意思,不难理解:
共享锁,是不同的事务会话可以共享使用的锁。排它锁,是一种只能被某个会话独享使用的锁。
对于不同的会话A和B,如果A已经获取了某条数据R上的共享锁,B也可以获取R上的共享锁,但是不能获取R上排它锁。此种情况下,B必须等待A释放共享锁后,才能获取排它锁。
如果A已经获取了某条数据R上的排它锁,那么B既不能获取R的共享锁和R的排它锁,必须等待A释放数据R上的排它锁;
等待,具体表现为阻塞
锁之间的兼容性如下表:
锁类型 | X | S |
---|---|---|
X | Conflict | Conflict |
S | Conflict | Compatible |
注意:此处的锁既可以表锁,也可以是行锁
从上表可以看到排它锁与其他的锁都互不兼容,共享锁之间是互相兼容。
那么如何才能获取数据R上共享锁or排它锁呢?
#表定义
CREATE TABLE `t` (
`id` int NOT NULL ,
`code` int NOT NULL ,
PRIMARY KEY (`id`)
);
select id from t where id = 1 for update; # 获取id=1数据行的排它锁
select id from t where id =1 lock in share mode; # 获取id=1数据行上的共享锁
2.意向锁(Intention Lock)
InnoDB支持多粒度的锁( multiple granularity locking),允许表锁和行锁同时存在。
InnoDB通过意向锁实现了多粒度的锁。
意向锁是表级别的锁,表示了该会话在之后会使用的锁(共享锁or排它锁)
意向锁有两种:共享意向锁(IS)、排它意向锁(XS)
- 共享意向锁表示一个事务即将需要获得该表中某些行的共享锁;
select lock in share mode; #可以获得IS锁
- 排它意向锁表示一个事务即将需要获得该表中某些行的排它锁;
select ... for update; # 获得XS锁
意向锁协议规定如下:
- 在一个事务获得某条(些)数据行R上的共享锁之前,该事务必须要获得该表上的共享意向锁(IS);
- 在一个事务获得某条(些)数据行R上的排它锁之前,该事务必须要获得该表上的排它意向锁(XS);
S锁,X锁,IS锁,IX锁兼容性如下表所示:
注意:以下锁均指的是表级别的锁,而非行级别的锁。
锁类型 | X | S | IX | IS |
---|---|---|---|---|
X | Conflict | Conflict | Conflict | Conflict |
S | Conflict | Compatible | Conflict | Compatible |
IX | Conflict | Conflict | Compatible | Compatible |
IS | Conflict | Compatible | Compatible | Compatible |
对于IX和X的组合,如果一个事务获得表的X锁,那么另一个事务如果想获得IX时就会被阻塞,等待X锁的释放。
意向锁仅仅表明意向,所以意向锁不会阻塞除扫描全表之外的(比如"LOCK TABLES ... WRITE")。
3.记录锁(Record lock)
记录锁是一种加在索引数据记录上锁,以防止其他事务对于该数据记录进行修改、删除。
例如:
select id from t where id = 1 for update; # 当前事务将获得id=1这行记录的排它记录锁
select id from t where id = 1 lock in share mode; # 当前事务将获得id=1这行记录的共享锁
通过update 、delete操作数据时,也会相应的获取对应索引记录的记录锁;
注意id为表t的主键,innoDB默认为主键加上了聚集索引(唯一性索引);
4.间隙锁
间隙锁是加载某个索引记录区间上的锁,间隙锁防止其他事务在锁定区间上插入新的记录,避免不可重复读。
那么如何获取间隙锁呢?
首先,我们为表t插入几条数据记录:
mysql> insert into t value (1,1);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value (8,8);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value (20,20);
Query OK, 1 row affected (0.00 sec)
现在数据库里面有3条数据,索引值为1,8,20。InnoDB会将索引id的区间进行划分为:
(-infinite,1),
(1,8),
(8,20),
(20,+infinite)四个区间;
例1 :
#事务会话A ,where 中 1 和 8 都是 索引值
select id from t where id between 1 and 8 lock in share mode; #获取区间(1,8)上的共享间隙锁;
其他事务如果想在(1,8)区间内插入新的数据记录,将会阻塞直到超时,如下所示:
#事务会话B
mysql> insert into t value (7,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
例2:
#事务A获取间隙锁,此时where中的4和14都不是实际存在的索引值
mysql> select id from t where id between 4 and 14 lock in share mode;
+----+
| id |
+----+
| 8 |
+----+
1 row in set (0.00 sec)
mysql> insert into t value (-1,"test");
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value(2,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (7,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (10,"test");
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value(21,"test");
Query OK, 1 row affected (0.00 sec)
通过实验,我们看到(1,8),(8,20)这两个区间被锁住了,禁止了其他事务在这两个区间中插入新的数据。
间隙锁,一个非常关键的点就是如何确定锁定区间范围?
InnoDB是通过对于where中范围查找中的最小值sMin和最大值sMax,其中锁定区间最小值lMin和最大值lMax。
- lMin的值为表索引值中小于or等于sMin中的最大值,对于sMin=4的情况,lMin=1。
- lMax的值为表索引值中大于or等于sMax中的最小值,对于sMax=14的情况下,lMax=20.
所以例2中区间的范围是(1,20)也即(1,8)和(8,20)。
注意新增加code不是唯一性索引
5. 临键锁(next-key lock)
临键锁是记录锁和间隔锁的结合,它既可以锁定索引记录也可以锁定索引记录之间的区间。临键锁主要的目的是解决幻读的问题。
在隔离级别是可重复性读(RR)下,并且innodb_locks_unsafe_for_binlog的值为OFF时,InnoDB默认使用的就是使用临键锁(next-key lock)。
在使用临键锁是,如果查询条件中含有唯一性索引,InnoDB会进行锁降级,变为记录锁。形式如:select id from t where id =1 for update;
如果查询条件不是唯一性索引,那么SQL会获得什么锁呢?
# 修改表t的结构,为code增加索引,该索引是普通索引(Secondary Index)并且非唯一性索引(Unique Index),并增加数据记录(15,8)
mysql> ALTER TABLE `t`
ADD INDEX `idx_code` (`code`) ;
mysql> insert into t value(15,8);
修改后,数据表的记录为:
(1,1)
(8,8)
(15,8)
(20,20)
从而对于索引 idx_code,我们可以得到其对应的区间分别为:
(-infinite,1],(1,8],(8,20],(20,+infinite)。
此时我们使用code为查询条件进行查询:
事务A:
mysql> select id from t where code = 8 lock in share mode;
+----+
| id |
+----+
| 8 |
| 15 |
+----+
2 rows in set (0.00 sec)
对于事务A,当执行完加锁读的语句后,InnoDB会使用next-key进行加锁,由于code不是唯一性索引,所以InnoDB不会对锁进行降级。由于表中存在两个索引,所以会分别进行加锁。对于聚集索引列id,由于加锁读命中了两条,id=8 和 id=15的记录会加锁。同时也会使用间隙锁,对code对应区间中的(1,8)和(8,20)进行加锁。
现在通过事务B对以上结论进行验证。
事务B:
mysql> insert into t value (-1,-1);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t value (2,2);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (9,9);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (21,21);
Query OK, 1 row affected (0.00 sec)
mysql> select * from t where id = 8 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 15 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
通过事务B的insert和加锁读语句,验证事务A获得锁。
在进一步讨论这个问题,如果code列没有索引,那么将会加什么锁呢?
事务A:
mysql> select id from t where code = 8 lock in share mode;
+----+
| id |
+----+
| 8 |
| 15 |
+----+
2 rows in set (0.00 sec)
由于code没有索引,为了找到code=8的数据记录,InnoDB将会进行全表扫描,此时全部的数据记录将会被锁定,其他事务无法进行操作。
接下来利用事务B进行验证:
事务B:
mysql> insert into t value (-1,-1);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (2,2);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (9,9);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t value (21,21);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 8 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 15 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 1 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> select * from t where id = 20 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
可以看到表t所有间隙都被锁定,验证上述分析结论。
6. 插入意向锁(Insert Intention )
InnoDB为了提高数据插入并发的效率,引入了插入意向锁。
插入意向锁是一种间隙锁,当事务视图想向一个区间插入数据时,首先获得插入意向锁。
插入间隙锁规定,如果多个事务向同一区间插入数据时,如果这些数据属于不同的位置(索引位置),那么之间的插入是互不影响的。
This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.
7. 自增锁(AUTO-INC lock)
自增锁是一种特殊表锁,这种锁用在对有自增列的表进行insert时。当事务A向自增表插入数据表,那么事务A将获得该表上自增锁,其他事务试图想该表插入数据时,必须等待事务释放该表的自增锁。
总结:
InnnoDB中锁的使用,与查询是否使用索引(全表扫描or not)、索引类型(聚集索引or普通索引)、查询范围(单个or区间)都有关系。但是掌握了各个锁的使用情况,就能根据具体的SQL判断出使用锁的类型。
参考:
- https://dev.mysql.com/doc/refman/5.6/en/innodb-storage-engine.html
- MySQL技术内幕-InnoDB存储引擎
网友评论