InnoDB存储引擎中的锁总结
官方文档:InnoDB Locking and Transaction Model
一、InnoDB存储引擎中的锁
1.共享锁和排它锁(Shared and Exclusive Locks)--行级别锁
InnoDB存储引擎实现了两种标准的行级锁:
- 共享锁(S):允许持有该锁的事务读取一行记录。
- 排它锁(X):允许持有该锁的事务更新或删除一行记录。
如果事务T1在行r上持有共享锁,另外的事务T2在行r上的请求锁,则会得到如下处理:
- 如果请求的是共享锁,则会立即授予。结果就是T1和T2同时获取到了行r上的共享锁。
- 如果请求的是排它锁,则不会立即授予。
如果事务T1在行r上持有排它锁,另一事务T2在行r上的请求任何类型的锁都不会立即授予,T2必须等待T1释放行r上的锁。
2.意向锁(Intention Locks)--表级别锁
InnoDB存储引擎支持多粒度锁定,即允许行锁和表锁同时存在。为了支持这种多粒度锁定,InnoDB存储引擎使用了意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意味着事务希望在更细粒度上进行加锁。意向锁是表级别的锁,用于指定事务稍后对表中的行需要哪种锁类型(共享锁或排它锁)。有两种类型的意向锁:
- 意向共享锁 intention shared lock (
IS
):表示事务打算对表中的各行设置共享锁 - 意向排它锁 intention exclusive lock(
IX
):表示事务打算对表中的各行设置排它锁
例如:
SELECT ... LOCK IN SHARE MODE 会设置一个意向共享锁(IS) SELECT ... FOR UPDATE 会设置一个意向排它锁(IX)
意向锁协议如下:
- 在事务可以获取表中某行的共享锁之前,它必须首先获取该表上的IS锁或更强的锁。
- 在事务可以获取表中某行的排它锁之前,它必须首先获取该表上的IX锁。
如果锁与现有锁兼容,则将其授予请求的事务,但如果与现有锁冲突,则不授予锁。 事务会等待直到冲突的现有锁被释放。意向锁的主要目的是表明有人正在锁定表中的行,或者打算锁定表中的行。
InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。由于InnoDB存储引擎支持的是行级别的锁,因此意向锁除了全表请求(例如LOCK TABLES ... WRITE)外,不阻止任何其他请求。
若将上锁的对象看做一颗树,那么对最下层的对象上锁,也就是最细粒度的对象上锁,那么分别需要对数据库A,表,页加上意向锁IX,最后对记录r加上X锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的完成。例如,事务T1试图依次在表1上加IX锁,并在记录r上加X锁,在加锁操作前,已经有另一事务T2已经对表1加了S表锁,此时由于IX和S不兼容,所以T1需要等待T2表锁操作的完成。
表级意向锁与行级锁的兼容性如下矩阵所示。
3.记录锁(Record Locks)
记录锁是在索引记录上进行加锁(回忆一下,InnoDB的B+树索引本身就会存储表中行记录数据)。例如,下面的sql语句会在c1=10这行索引记录上加锁,阻止任何其它事务在c1=10这行上进行插入、更新和删除操作。
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
记录锁总是锁住索引记录,即使表没有定义索引。因为InnoDB存储引擎会创建隐式的聚集索引,并将其用于记录锁。See Section 14.6.2.1, “Clustered and Secondary Indexes”.
注意:下面的SELECT是快照读(SnapShot Read),并不会进行加锁操作。后文会再次提到。
SELECT c1 FROM t WHERE c1 = 10
4.间隙锁(Gap Locks)
间隙锁是对索引记录之间,或第一个索引记录之前,或最后一个索引记录之后的间隙的锁定。例如,以下sql语句会锁定(10-20)这个范围,其它事务插入c1=15时将会被阻塞,因为(10-20)这个范围已经被锁定了。
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE
间隙可能跨越单个索引值,多个索引值,甚至为空。
间隙锁不适用于唯一索引情形
对于使用唯一索引来搜索唯一行的语句,不会进行间隙锁定(这不包括搜索条件仅包含多列唯一索引的某些列的情况;在这种情况下,会发生间隙锁定)。例如,对于下面的Sql语句,
如果id列具有唯一索引,则仅会对id=100的行进行记录锁定,而不进行间隙锁定,所以其他会话可以在该记录前的间隙中进行插入。如果id未建立索引或具有非唯一索引,则该语句会锁定前面的间隙。
#【如果id列有唯一索引,则仅锁住id=100这行记录,不会进行间隙锁定】 #【如果id列无索引或为非唯一索引,则会锁住id=100这行记录,并锁定之前的间隙】 SELECT * FROM child WHERE id = 100;
READ COMMITTED事务隔离级别会禁用间隙锁
间隙锁定可以被显式的禁用,只需将事务隔离级别更改为READ COMMITTED或启用innodb_locks_unsafe_for_binlog系统变量(现已弃用)。此时,对于搜索和索引扫描,间隙锁定将被禁用,并且间隙锁定仅用于外键约束检查和重复键检查。
使用READ COMMITTED隔离级别或启用innodb_locks_unsafe_for_binlog还有其他效果。MySQL评估WHERE条件后,会释放不匹配行的记录锁。对于UPDATE语句,InnoDB进行“半一致”读取,以便将最新的提交版本返回给MySQL,以便MySQL可以决定该行是否与UPDATE的WHERE条件匹配。
5.临键锁(Next-Key Locks)--解决幻读问题
next-key锁是记录锁和间隙锁的结合,它是谓词锁(predict lock)的一种改进。其设计的目的是为了解决Phantom Problem(幻影问题,幻读),也就是阻止多个事务将记录插入到同一个范围内而导致的幻读问题。InnoDB存储引擎在默认的事务隔离级别REPEATABLE READ下,使用next-key锁定进行搜索和索引扫描,从而避免了幻像行问题(请参见第14.7.4节“幻像行”)。
InnoDB执行行级锁定的方式是,当它搜索或扫描表索引时会在遇到的索引记录上设置共享或互斥锁(行级锁定)。因此,行级锁实际上是索引记录锁。索引记录上的next-key锁定也会影响该索引记录之前的“间隙”(间隙锁定)。也就是说,next-key锁锁定是索引记录锁定加上索引记录之前的间隙上锁定。
如果一个会话在索引中的记录r上具有共享或排他锁,则另一会话不能按照索引顺序在r之前的间隙中插入新的索引记录。假如,索引包含值10、11、13和20,则next-key锁可以锁定的间隔为(圆括号表示不包括,方括号表示包括)以下范围。对应的还有previous-key锁(包含左侧,不包含右侧)
(-∞, 10] (10, 11] (11, 13] (13, 20] (20, +∞) 对于最后一个间隔,next-key锁定将间隙锁定在索引中的最大值上方,并且“超级”伪记录的值高于索引中的任何实际值。最高不是真正的索引记录,因此,实际上此next-key锁定仅锁定最大索引值之后的间隙。
如果事务T1通过next-key锁锁定了区间(10,11],(11,13],当插入新记录12时锁定范围变成了:(10,11],(11,12],(12,13]。
临键锁不适用于唯一索引情形(临键锁降级为记录锁)
①当查询的索引含有唯一属性时,InnoDB仅进行记录锁定,而不进行间隙锁定。可以理解为InnoDB对next-key锁进行优化,降级为了Record Lock。
例如,t表中a为主键且唯一,当会话1执行以下语句:
SELECT * FROM t WHERE a=5 FOR UPDATE;
之后,在会话2执行以下语句会执行成功。
INSERT INTO t SELECT 4;
②next-key锁降级为Record Lock时,仅在查询的列是唯一索引的情况下。如果是辅助索引,则情况完全不同。
例如,t表中a为主键且唯一,b为辅助索引列。在会话1中执行下列sql语句
#由于b为辅助索引,所以加next-key锁,锁定范围(1-3]。InnoDB还会对辅助索引下一个键(也就是索引记录6)加gap lock,锁定范围(3,6) SELECT * FROM t WHERE b=3 FOR UPDATE;
在新会话2中分别执行以下的sql都会被阻塞。
#阻塞。因为会话1在索引记录a=5上已经加了X锁。 SELECT * FROM t WHERE a=5 LOCK IN SHARE MODE; #阻塞。主键插入4没问题,但插入辅助索引值2在范围(1,3)中,因此阻塞。 INSERT INTO t SELECT 4,2; #阻塞。插入的主键6没有被锁定,插入辅助索引值5也不在(1,3)之间,但其在另一个锁定的范围(3,6)中,因此阻塞。 INSERT INTO t SELECT 6,5;
6.插入意向锁(Insert Intention Locks)
插入意向锁是间隙锁的一种类型,它是在行插入之前设置的。如果多个事务不是在间隙的同一个位置插入,则无需等待插入到同一索引间隙中的多个事务。
例如,假设有一个记录索引包含键值4和7,两个事务分别插入5和6,那么每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会彼此阻塞,因为行并不冲突。
7.自增长锁(AUTO-INC Locks)
官方文档:AUTO-INC Locks 和 AUTO_INCREMENT Handling in InnoDB
在InnoDB存储引擎中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter)。当对含有自增长的计数器的表进行插入操作时,这个计数器会被初始化,执行如下语句来得到计数器的值:
SELECT MAX(auto_inc_col) FROM t FOR UPDATE;
插入操作会根据自增长计数器+1赋值给自增长列。这个实现方式称为AUTO-INC Locking。这种锁其实采用的是一种特殊的表锁机制,由事务在插入进具有AUTO_INCREMENT列的表时获得。
为了提高插入的性能,锁不是在一个事务完成后释放,而是在完成对自增长值插入的SQL语句后立即释放。虽然通过这种方式提升了效率,但仍存在一些性能问题。
①首先,对于有自增长的列的并发插入性能较差,事务必须等待前一个插入的完成(虽然不用等待事务的完成)。
②其次,对于INSERT……SELECT的大数据量的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。
轻量级互斥量的自增长实现机制
从Mysql5.1.22版本中开始,InnoDB存储引擎中提供了一种轻量级互斥量的自增长实现机制,大大提高了自增长值插入的性能。并提供了一个innodb_autoinc_lock_mode
参数,用来控制自增长的模式(默认值为1),它使您可以选择如何在可预测的自动增量值序列与插入操作的最大并发性之间进行权衡。For more information, see Section 14.6.1.6, “AUTO_INCREMENT Handling in InnoDB”.
注意项
InnoDB中的自增长实现与MyISAM不同,MyISAM存储引擎是表锁设计,自增长不用考虑并发插入的问题。
另外,在InnoDB中,自增长的列必须是索引,且必须是索引的第一个列。如果不是第一个列,则Mysql会抛出异常。而MyISAM存储引擎不存在这样的问题。
8.谓词锁(Predicate Locks for Spatial Indexes)
InnoDB支持对包含空间列的列进行空间(SPATIAL)索引(参考Section 11.4.8, “Optimizing Spatial Analysis”)
为了处理涉及SPATIAL索引的操作的锁定,next-key锁定不能很好地支持REPEATABLE READ或SERIALIZABLE事务隔离级别。多维数据中没有绝对排序概念,因此尚不清楚哪个是next key。
为了支持具有SPATIAL索引的表的隔离级别,InnoDB使用谓词锁。SPATIAL索引包含最小边界矩形(MBR)值,因此InnoDB通过在用于查询的MBR值上设置谓词锁定来强制对索引进行一致的读取。其他事务不能插入或修改将匹配查询条件的行。
9.(补充)外键和锁
外键用于引用完整性的约束检查。在InnoDB存储引擎中,如果没有显式的对外键列加索引,InnoDB会自动加上一个索引,因为这样可以避免锁表。
外键检查时使用一致性锁定读,而非一致性非锁定读
对于外键值的插入或更新,因为要进行约束检查,所以首先需要查询父表中的记录,即SELECT父表。SELECT父表时并非使用的一致性非锁定读(MVCC)的方式,因为会发生数据一致性问题,所以使用的SELECT……LOCK IN SHARE MODE这种一致性锁定读方式,即主动对父表加一个S锁。如果此时父表上已经加了X锁,则对父表加S锁的操作必然阻塞。
例如,两个会话依次进行以下操作,两个会话中的事务都没有进行提交或回滚,则会话B会被阻塞。
#会话A(先) BEGIN DELETE FROM parent WHERE id=3; # 在表上加X锁 #会话B(后) BEGIN INSERT INTO child SELECT 2,3; #第2列为外键,插入时会进行约束检查,查询父表,需要对父表加S锁,从而阻塞。
会话A在做删除时会加X锁,会话B在插入时会进行约束检查,需要查询父表,所以要对父表加S锁,从而阻塞。
为什么使用一致性锁定读而非一致性非锁定读?
上面说过,SELECT父表时使用的一致性的锁定读,设想如果使用的一致性非锁定读(MVCC)方式会怎样?会话B会读到父表存在id=3的记录,可以进行插入。但如果会话A将事务提交了,则父表就不存在id=3的记录了。显然父表和子表就会存在不一致的情况。
二、一致性非锁定读和一致性锁定读
1.一致性锁定读(consistent locking read)--加锁方式
对于隐式的加锁,InnoDB会根据隔离级别在需要的时候自动加锁。
一致性锁定读,顾名思义就是在读取时会进行加锁,即显式地对数据库读取操作进行加锁以保证数据一致性。InnoDB存储引擎对于SELECT语句也支持显示的锁定,但它们不属于SQL规范。InnoDB存储引擎对于SELECT语句支持两种一致性的锁定读(locking read)操作:
- SELECT……FOR UPDATE:对读取的行加一个X锁(排它锁),其它事务不能对已锁定的行加上任何锁。当然,此时仍然可以进行一致性非锁定读(MVCC,读快照)。
- SELECT……LOCK IN SHARE MODE:对读取的行加一个S锁(共享锁),其它事务可以对被锁定的行加S锁(即可以读)。但加X锁则会被阻塞直至事务提交(即写被阻塞)。
另外,Mysql也支持LOCK TABLES,这是在服务器层而非存储引擎层实现的,它们有自己的用途,并不能用来代替事务(的加锁)。
2.一致性非锁定读(Consistent Nonlocking Read)--MVCC方式
官方文档:14.7.2.3 Consistent Nonlocking Reads
一致性非锁定读指的是一个读操作发现读取的行正在进行DELETE或UPDATE操作,这时读取操作不会去等待行上锁的释放,相反会去读取行的一个快照版本。这是InnoDB存储引擎默认隔离级别(REPEATABLE READ)下的读取方式。实际上在READ COMMITTED和REPEATABLE READ隔离级别下,InnoDB存储引擎使用的都是非锁定的一致性读。
- 在READ COMMITTED(默认)隔离级别下,对于快照数据的读取,非一致性读总是读取被锁定行的最新一份快照数据。
- 在REPEATABLE READ隔离级别下,对于快照数据的读取,非一致性读总是读取事务开始时的行数据版本。从数据库理论角度来讲,其违反了事务ACID中的I的特性,即隔离性。
一个行记录可能有多个快照版本,所以称这种技术为行多版本技术,由此带来的并发控制称之为多版本并发控制(multi-versioned concurrency control,MVCC)。
三、InnoDB中不同sql语句使用的锁
官方文档:14.7.3 Locks Set by Different SQL Statements in InnoDB
Select
SELECT ... FROM
是一致性读,读取数据库的快照,且不会设置任何锁,除非事务隔离级别设置成了SERIALIZABLE。
当设置为SERIALIZABLE隔离级别时,搜索会在它遇到的索引记录上设置共享的next-key锁。但对于使用唯一索引来搜索唯一行时,仅在索引上使用记录锁(next-key锁降级为record lock)。
SELECT ... FOR UPDATE SELECT ... LOCK IN SHARE MODE
在扫描行时获得锁,并有望针对不符号查询条件的结果集的行释放锁(例如,不满足where条件)。但是,在某些情况下行可能不会立即解锁,因为结果行与其原始源之间的关系在查询执行过程中会丢失。例如,在UNION查询中,在评估是否符合结果集之前,表中被扫描(并锁定)行可能被插入到临时表中。在这种情况下,临时表中的行与原始表中的行之间的关系将丢失,并且直到查询执行结束后者才被解锁。
SELECT ... FOR UPDATE:会在搜索时遇到的索引记录上设置排它的next-key锁。但对于使用唯一索引搜索唯一行的行来锁定的语句,仅在索引上使用记录锁。
SELECT ... LOCK IN SHARE MODE:会在搜索时遇到的索引记录上设置共享的next-key锁。但对于使用唯一索引搜索唯一行的行来锁定的语句,仅在索引上使用记录锁。
对搜索遇到的索引记录, SELECT ... FOR UPDATE 会阻止其他会话执行SELECT ... LOCK IN SHARE MODE,或在某种事务隔离级别下的读取操作。一致性的读将忽略读取视图中存在的记录上设置的任何锁定。
Update
UPDATE ... WHERE ...
会在搜索遇到的每条记录上设置排它的next-key锁。但是,对于使用唯一索引搜索唯一行的行来锁定的语句,仅在索引上使用记录锁。
当UPDATE修改聚簇索引记录时,将对受影响的辅助索引记录进行隐式锁定。在插入新的二级索引记录之前执行重复检查扫描时,以及在插入新的二级索引记录时,UPDATE操作还会在受影响的二级索引记录上获得共享锁。
Delete
DELETE FROM ... WHERE ...
会在搜索遇到的每条记录上设置排它的next-key锁定。但是,对于使用唯一索引搜索唯一行的行来锁定的语句,仅在索引上使用记录锁。
Insert
insert
在插入的行上设置排它锁(X)。该锁是索引记录锁,不是next-key锁(即没有间隙锁),并且不会阻止其它会话插入到插入行之前的间隙中。
在插入行之前,会设置一种称为插入意向间隙锁(Insert Intention Lock)的间隙锁。前面已经介绍过了,如果多个事务不是在间隙的同一个位置插入,则无需等待插入到同一索引间隙中的多个事务。
更多sql语句使用的锁情况,请参考官方文档。
INSERT ... ON DUPLICATE KEY UPDATE REPLACE INSERT INTO T SELECT ... FROM S WHERE ... CREATE TABLE ... SELECT ... LOCK TABLES
三、死锁
官方文档:
死锁的可能性并不会受到事务隔离级别的影响,因为事务隔离级别仅改变读操作的行为,而死锁因写操作才会发生。
数据库解除死锁的方式有以下两种:
①锁超时机制(被动):
解决死锁最简单的方式就是使用锁超时,即当两个事务互相等待时,当一个事务等待锁的时间超过设置的阈值时,将其回滚,另一个事务就能继续进行了。在InnoDB中,可以通过参数innodb_lock_wait_timeout来设置超时时间。
这种方式虽然简单,但通常来说不太好。若超时的事务所占权重比较大,比如更新多行的事务,将其回滚花费的时间可能比另一个事务执行占用的时间大的多。(这种情况,将另一个事务回滚,让占权重大的事务继续进行代价最小)
②死锁检测(主动):wait-for gragp(等待图)
除了超时机制,当前数据库还都普遍采用wait-for gragp(等待图)的方式来进行死锁检测。与超时的解决方案相比,这是一种更为主动的死锁检测方式。InnoDB存储引擎也采用的这种方式。在每个事务请求锁并等待时,通过检测图中是否存在回路来判断是否存在死锁。如果检测存在死锁,通常InnoDB存储引擎会选择回滚undo量最小的事务。
四、锁升级
锁升级(Lock Escalation)是指当前锁的粒度降低。比如,数据库可以将一个表的1000个行锁升级为1个页锁,或者将页锁升级为表锁。
InnoDB存储引擎不存在锁升级的问题。因为其不是根据每个记录来产生行锁的,相反,其根据每个事务访问的每个页进行管理的,采用的是位图的方式。因此不管一个事务锁住页中一个记录还是多个记录,其开销通常都是一样的。
总结
1.锁分为行级别锁和表级别锁,按照属性来分,行级别锁可分为共享锁(S)和排它锁(X),它们都属于悲观锁的范畴。
2.为了支持细粒度锁定,InnoDB设计了意向锁,它是表级别锁。
3.行级锁有3种算法:记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)。
记录锁仅锁住索引记录本身,间隙锁锁住区间范围但不锁定索引记录,而临键锁既锁住索引记录本身又锁住区间。
间隙锁仅在可重复读隔离级别下才有效,在读已提交隔离级别下无效。
4.引入间隙锁,可能会产生死锁的问题。