第六章 锁(上)
6.1 什么是锁
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。数据库使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。InnoDB存储引擎的锁实现提供一致性的非锁定读、行级锁支持。行级锁没有相关额外的开销,并可以同时得到并发性和一致性。
6.2 lock和latch
在数据库中,lock和latch都可以被称为“锁”。但是两者有着截然不同含义。
latch一般被称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差。在InnoDB存储引擎中,latch又可以分为mutext(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测机制。
lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如大多数数据库中一样,是有死锁机制的。用户可以通过 show engine innodb status及information_schema架构下的表innodb_trx,innodb_locks,innodb_lock_waits来观察锁的信息。
6.3 InnoDB存储引擎中的锁
6.3.1 锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁:
- 共享锁(S Lock),允许事务读一行数据
- 排他锁(X Lock),允许事务删除或者更新一行数据
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。但若有其他的事务T3想获取行r的排他锁,则其必须等待事务T1,T2释放行r上的共享锁——这种情况称为锁不兼容。下表显示了共享锁和排序锁的兼容性。
image.png
此外,MySQL支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁。
若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁。例如,如果想要对页上的某条记录r进行上X锁,那么分别需要数据库、表、页上意向锁IX,最后对记录r上X锁。若其中任何一个部分导致等待,那该操作需要等待粗粒度锁的完成。
InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在事务中揭示下一行将被请求的锁类型。其支持两种意向锁:
- 意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁
- 意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。故表级意向锁和行级锁的兼容性如下表所示:
image.png
6.3.2 一致性非锁定读
一致性的非锁定读(consistent nonlocking read)是指InnoDB存储引擎通过多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE或UPDATE操作,这是读取操作不会因此去等待行上锁的释放。相反地,InnoDB存储引擎会去读取行的一个快照数据。之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,每行记录可能有多个版本。该实现是通过undo段来完成的。而undo用来在事务中回滚数据,因此快照数据本身是没有额外开销的。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史数据进行修改操作。
非锁定读机制极大地提高了数据库的并发性。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式是不同的,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定一致性读,但是对于快照数据的定义也各不相同。
在事务隔离级别READ COMMITED和PEPEATABLE READ(InnoDB存储引擎的默认事务隔离级别)下,InnoDB存储引擎使用非锁定的一致性读。然而,对于快照数据的定义却不相同。在READ COMMITED事务隔离级别下,对于快照数据,非一致性总是读取被锁定行的最新的一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。
示例如下:
parent表.png 插入数据.png 流程.png
6.3.3 一致性锁定读
在默认配置下,即事务的隔离级别为 REPEATABLE READ模式下,InnoDB存储引擎的select 操作使用一致性非锁定读。但是在某些情况下,用户需要显示地对数据库读取操作进行加锁以保证数据逻辑的一致性。而这要求数据库支持加锁语句,即使是对于select的只读操作。InnoDB存储引擎对于select语句支持两种一致性的锁定读(locking read)操作
- select ... for update
- select... lock in share mode
select...for update 对读取的行记录加一个X锁,其他事物不能对已锁定的行加上任何锁。select...lock in share mode对读取的行记录加上一个S锁,其他事物可以向锁定的行加S锁,但是如果加X锁,则会被阻塞。
对于一致性非锁定读,即使读取的行已被执行了SELECT...FOR UPDATE,也是可以进行读取的(MVCC机制)。此外,SELECT... FOR UPDATE,SELECT... LOCK IN SHARE MODE必须在一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句SELECT锁定语句是,务必加上begin,start transaction或者set autocommit=0
6.3.4 自增长与锁
自增长在数据库中是非常常见的一种属性。在InnoDB存储引擎的内部结构中,对每个含有自增长值的表都有一个自增长计数器。当对含有自增长的计数器的表进行插入操作时,这个计数器会被初始化,可以执行如下语句来得到计数器的值:
select max(auto_inc_col) from t for update
插入操作会依据这个自增长的计数器值加1赋予自增长列。这个实现方式称做AUTO-INC Locking。这种锁其实是采用一种特殊的表锁机制,为了提高插入操作的性能,锁不是在一个事务完成之后才释放,而是在完成对自增长值插入的SQL语句后立即释放。
虽然AUTO-INC Locking从一定程度上提高了并发插入的效率,事务必须等待前一个插入的完成(虽然不用等待事务的完成)。其次,对于INSERT···SELECT的大数据量的插入会影响插入的性能,因为另一个事务中的插入会被阻塞。
从MySQL5.1.22版本开始,InnoDB存储引擎提供了一种轻量级互斥量的自增长实现机制,这种机制大大提供了自增长插入的性能。并且从该版本开始,InnoDB存储引擎提供了一个参数innodb_autoinc_lock_mode来控制自增长的模式,该参数的默认值为1
此外,还需要特别注意的是InnoDB存储引擎中自增长的实现和MyISAM不同,MyISAM是表锁设计,自增长不用考虑并发插入的问题,因此在master上用InnoDB,在slave上用MyISAM的replication架构下,用户必须考虑这种情况。
在InnoDB存储引擎中,自增长值的列必须是索引,同时必须是索引的第一个列。如果不是第一个列,则MySQL数据库会抛出异常,而MyISAM存储引擎没有这个问题。
6.3.5 外键和锁
在InnoDB存储引擎中,对于一个外键列,如果没有显示地对这个列加索引,InnoDB存储引擎自动对其加一个索引,因为这样可以避免表锁
对于外键值的插入或更新,首先需要查询父表中的记录,即SELECT父表。但是对于父表的SELECT操作,不是使用一致性非锁定读的方式,因为这样会发生数据不一致的问题,因此这时使用的是SELECT··· LOCK IN SHARE MODE方式,即主动对父表加一个S锁。如果这时父表上已经加上了X锁,子表的操作会被阻塞。如下图所示
image.png
6.4 锁的算法
6.4.1 行锁的3中算法
InnoDB存储引擎有3中行锁算法,其分别是:
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身
Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:
(负无穷,10]
(10,11]
(11,13]
(13,20]
(20,正无穷)
采用Next-Key Lock的锁定技术称为Next-Key Locking。其设计目的是为了解决Phantom Problem(幻读),而使用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁的一种改进。然而,当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。举个例子如下:
create table t (a int primary key);
insert into t values(1),(2),(5);
在会话A中首先对a=5进行X锁定。而由于a是主键且唯一,因此锁定的仅是5这个值,而不是(2,5]这个范围,这样在会话B中插入值4而不会阻塞,可以立即插入并返回,即锁定有Next-Key Lock算法降级为了Record Lock,从而提供了应用的并发性能
Next-Key Lock降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,则情况完全不同。
image.png image.png
表z的列b是辅助索引,若在会话A中执行下面的SQL语句:
select * from z where b=3 for update
很明显,这是SQL语句通过索引列b进行查询,因此其使用传统的Next-Key Locking技术加锁,并且由于有两个索引,其需要分别进行锁定。对于聚集索引,其仅对列a等于5的索引加上Record Lock。而对于辅助索引,其加上的是Next-Key Lock,锁定范围是(1,3],特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁,因此,若在新会话B中运行下面的SQL语句,都会阻塞:
select * from z where a = 5 lock in share mode
insert into z select 4,2
insert into z select 6,5
第一个SQL语句不能执行的原因是会话A中已经对聚集索引a=5加上了X锁,因此执行会被锁定。
第二个SQL语句不能执行的原因是辅助索引在锁定范围(1,3] 之内。
第三个SQL语句不能执行的原因是辅助索引在锁定范围(3,6] 之内。
而下面的sql语句可以立即执行
insert into z select 8,6
insert into z select 2,0
insert into z select 6,7
Gap Lock的作用是为了阻止多个事务将记录插入到同一个范围内,、这能避免Phantom Problem问题的产生。
用户可以通过以下两种方式来显式地关闭Gap Lock:
- 将事务的隔离级别设置为READ COMMITTED
- 将参数innodb_locks unsafe_for_binlog设置为1
在上述配置下,除了外键约束和唯一性检查依然需要Gap Lock,其余情况仅使用Record Lock进行锁定。
若唯一索引由多个列组成,而查询仅是查找多个唯一索引中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。
6.4.2 解决Phantom Problem问题
在默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem问题。这点不同于其他数据库,如Oracle数据库,因为其可能需要在SERIALIZABLE的事务隔离级别下才能解决Phantom Problem。
Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不能的结果,第二次的SQL语句可能会返回之前不存在的行。
表t由1,2,5这三个值组成,若这时事务T1执行如下的SQL语句:
select * from t where a > 2 for update
这时事务T1还没有提交,上述应该返回5这个结果。若于此同时另一个事务插入了 4 这个值,并且数据库允许该操作(隔离级别是RC可以操作),再次执行事务T1的SQL语句,会返回结果4,5。这违反了事务的隔离性,即当前事务能看到其他事务的结果。InnoDB存储引擎采用Next-Key Locking的算法避免了Phantom Problem。
InnoDB存储引擎默认的事务隔离级别是RR,在该隔离级别下,其采用Next-Key Locking的方式来加锁。而在事务隔离级别RC下,其仅采用Record Lock,因此上述的示例中,隔离级别是RC的