翻译自MySQL官方文档:InnoDB Locking。
本文介绍Innodb使用到的锁的类型:
- 共享锁和排它锁
- 意向锁
- 记录锁
- 间隙锁(Gap locks)
- Next-Key Lock
- 插入意向锁
- 自增锁(Auto-INC Locks)
- 空间索引的谓词锁
共享锁和排它锁
Innodb实现了2种标准的行级锁,即共享锁(S锁)和排它锁(X锁)。
- 持有共享锁的事务可以读取记录。
- 持有排它锁的事务可以更新、删除记录。
如果事务T1
持有记录r
的共享锁,则事务T2
:
- 也可以马上获得记录
r
的共享锁。T1
和T2
可以同时持有共享锁。 - 但是不能马上获得记录
r
的排它锁。
如果事务T1
持有记录r
的排它锁,则事务T2
不能马上获取记录r
的排它锁或共享锁,事务T2
需要等待,直到T1
释放锁。
意向锁
Innodb支持多粒度锁,即表锁和行锁。例如,可以使用LOCK TABLES .... WRITE
语句给表格加上表级的排它锁。Innodb使用意向锁来实现多粒度锁定。意向锁是表级锁,它表示稍后事务会申请这个表格的哪些类型的行锁。意向锁有2种类型:
- 意向共享锁(IS),表示事务准备在某些记录上加上共享锁。
- 意向排它锁(IX),表示事务准备在某些记录上加上排它锁。
例如,SELECT ... LOCK IN SHARE MODE
会加上一个意向共享锁,SELECT ... FOR UPDATE
会加上一个意向排它锁。
意向锁的规则如下:
- 事务在获取某些行的共享锁之前,必须先获取该表格的意向共享锁(或更强的锁)。
- 事务在获取某些行的排它锁之前,必须先获取该表格的意向排它锁。
各种表级锁的兼容性如下:
X |
IX |
S |
IS |
|
---|---|---|---|---|
X |
冲突 | 冲突 | 冲突 | 冲突 |
IX |
冲突 | 兼容 | 冲突 | 兼容 |
S |
冲突 | 冲突 | 兼容 | 兼容 |
IS |
冲突 | 兼容 | 兼容 | 兼容 |
如果事务申请的锁,跟其它事务已经获取的锁是兼容的,那么就可以马上申请到;否则出现锁冲突时,申请锁的事务会被阻塞,直到其它事务把锁释放掉。如果事务申请锁而导致死锁出现时,会产生一个错误。
意向锁只会阻塞对整个表加锁的请求,例如LOCK TABLES ... WRITE
。意向锁的主要用途是:表明其它事务锁住了、或者即将表格种的某些行。
通过SHOW ENGINE INNODB STATUS
语句输出事务信息时,意向锁的输出类似于:
TABLE LOCK table
test
.t
trx id 10080 lock mode IX
记录锁
记录锁是加在索引上的锁。例如,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
会阻止其它事务插入、更新或删除t.c1=10
的记录。
如果表格没有定义任何索引,也没有指定主键索引时,Innodb会自动创建一个隐藏的列作为主键索引。
通过SHOW ENGINE INNODB STATUS
语句输出事务信息时,记录锁的输出类似于:
1 | RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` |
间隙锁
间隙锁,用于锁住索引之间的间隙,或者索引第一条记录之前、最后一条记录之后的区间。例如,执行SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
语句会阻止其它事务插入t.c1=15
的记录,不管目前表格中有没有t.c1=15
的记录,因为间隙锁会锁住已有记录之间的间隙。
一个间隙可能锁住索引的一个值、多个值,也可能0个值。
间隙锁是用于权衡性能和并发性的一个方案,只在某些隔离级别下会用到。
当使用唯一索引来查找一行唯一的记录时,不会用到间隙锁。但是如果唯一索引由多个字段组成,WHERE
条件中只有部分字段,这种情况还是会用到间隙锁。例如,id
字段是唯一的,下面的语句会在id=100
的记录上加上行锁,但不会阻止其它事务往id=100
之前的间隙插入记录:
1 | SELECT * FROM child WHERE id = 100; |
如果id
字段没有索引,或者不是唯一索引,这条SQL会锁住id=100
之前的记录。
值得注意的是,不同的事务可以同时持有冲突的间隙锁。例如,事务A持有一个共享的间隙锁(gap S-lock),事务B可以同时持有同一个间隙的排它间隙锁(gap X-lock)。间隙锁允许冲突的原因是:如果所有中的一条记录被删除,则必须合并不同事务持有的间隙锁。
间隙锁唯一的用处是:阻止其它事务往间隙中插入记录。间隙锁可以共存,事务获取到一个间隙锁后,并不会阻止其它事务获取同一个间隙的间隙锁。共享间隙锁和排它间隙锁没有区别,它们也不会互相阻塞。
间隙锁可以被显式的禁用:通过把隔离级别设置为读已提交(READ-COMMITTED),或者启用配置项innodb_locks_unsafe_for_binlog
(现在不推荐使用这个配置)。这时,索引查找、索引扫描都不会使用间隙锁,只有检查外键约束、唯一索引时才会使用。
把隔离级别设置为读已提交,或者修改innodb_locks_unsafe_for_binlog
配置还有其它影响:MySQL服务层判断WHERE
中的条件与记录不匹配时,会释放记录锁。对于UPDATE
语句来说,Innodb使用“半一致读”:总是读取最新的已提交的版本,返回给MySQL服务层,这样MySQL就能判断这些行是否满足UPDATE
中的WHERE
条件。
Next-Key 锁
Next-Key锁是索引上的记录锁,和索引之前的间隙锁的组合。
Innodb加行级锁的方式: 当查找或者扫描索引时,给查找过程中遇到的记录加上排它锁或共享锁。所以说,行级锁其实就是记录锁。加在索引记录上的Next-key锁会对索引记录之前的间隙有影响,也就是说:Next-key锁是一个记录锁,加上索引之前的间隙锁。如果一个事务在记录R
上加上了共享锁或者排它锁,其它事务就无法在R
之前(按照索引顺序)的间隙插入数据。
假设一个索引现在已有的值为:10, 11, 13和20. 则Next-key可能锁定的区间如下:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
对于最后一个区间,Next-key锁住了索引中的最大值到正无穷的区间,虽然索引中并没有“正无穷”这个值。
默认情况下,Innodb使用可重复读(REPEATABLE READ)隔离级别。在这个隔离级别下,Innodb会在查找时,给遇到的记录加上Next-key锁,用来防止幻读。
通过SHOW ENGINE INNODB STATUS
语句输出事务信息时,Next-Key锁的输出类似于:
1 | RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` |
插入意向锁
插入意向锁是一种间隙锁,在INSERT
语句插入数据之前会加上插入意向锁。插入意向锁的用处:向同一个索引间隙中插入数据的事务,如果它们没有插入到间隙中的相同位置,就不需要互相等待。假设现在索引中有4和7两个值,有2个事务分别想插入5和6,则每个事务在获得插入行的排它锁之前,都会使用插入意向锁锁住4~7之间的间隙,但是插入意向锁并不会互相阻塞,因为2个事务插入的是不同的位置。
下面的例子演示了一个事务在获取插入行的排它锁之前,获取了插入意向锁。例子中一共有2个客户端A和B。
客户端A首先创建一个表格,插入2行记录:90和102, 然后开启一个事务,获取了id>100
的记录的排它锁,以及一个id<102
的一个间隙锁。
1 | mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB; |
客户端B启动一个事务,让后往间隙中插入数据。事务在等待获取排它锁时,获取了一个插入意向锁:
1 | mysql> START TRANSACTION; |
通过SHOW ENGINE INNODB STATUS
语句可以看到以下输出:
1 | RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child` |
自增锁(AUTO-INC Locks)
自增锁是一个特殊的表级锁,向带有自增字段的表格插入数据时会用到。一个最简单的例子:如果一个事务正在向表格中插入数据,其它往同一个表格插入数据的事务必须等待,这样生成的自增ID才是连续的。
通过innodb_autoinc_lock_mode
参数可以控制自增锁的算法,你可以选择更好的并发性,但是放弃自增字段的连续性。
空间索引的谓词锁
Innodb支持给空间类型的字段增加空间索引。
对于空间索引来说,Next-key并不能很好的支持可重复读和可序列化隔离级别,因为在空间索引中,没有绝对的排序的概念,不能确定哪一个是索引的下一个值。
Innodb使用谓词锁来支持空间索引的各个事务隔离级别。空间索引包含最小边界矩形(MBR)值,因此Innodb给用于查询的MBR值加上谓词锁来实现对空间索引进行一致性读取,其它事务不能修改、插入与当前事务查询条件匹配的行。