InnoDB 存储引擎中的锁
1.什么是锁?
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
并且人们总是认为行级锁总会增加开销。实际上,只有当实现本身会增加开销时,行级锁才会增加开销。InnoDB 存储引擎不需要锁升级,因为一个锁和多个锁的开销是相同的。
2.lock 与 latch
这里还要区分锁中容易混淆的两个概念 lock 和latch。在数据库中,lock 与 latch 都可以被称为“锁”。但是两者的含义截然不同:
latch:一般称之为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常查。在 InnoDB 储存引擎中,latch 又分为 mutex(互斥锁) 和 rwlock (读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。
lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般 lock 的对象仅在事务 commit 或者 rollback 后进行释放(不同事务隔离级别释放的时间可能不同)。此外,lock,正如在大多数数据库中一样,是有死锁机制的。
3.锁的类型
InnoDB 储存引擎实现了两种标准的行级锁:
- 共享锁(S Lock),允许事务读取一行数据。
- 排他锁(X Lock),允许事务删除或更新一行数据。
表格:排他锁和共享锁的兼容性
| X Lock | S Lock | |
|---|---|---|
| X Lock | 不兼容 | 不兼容 |
| S Lock | 不兼容 | 兼容 |
由表格可知,X Lock 与任何锁都不兼容,而 S Lock 仅和 S Lock 兼容。需要特别注意的是,S Lock 和 X Lock 都是行锁,兼容是指的对同一记录(row)锁的兼容性情况。
此外,InnoDB 储存引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB 储存引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁。
若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,首先需要对粗粒度的对象进行上锁。如图所示,如果需要对页上的记录 r 上 X 锁,那么分别需要对数据A、表、页上意向锁IX,最后对记录 r 上 X 锁。若其中任何一个部分导致等待,那么该操作需要等待粗粒度锁的释放。举例来说,在对记录 r 加 X 锁之前,已经有事务对表1进行了 S 表锁,那么表1上已经存在 S 锁,之后事务需要对记录 r 在表1上加上 IX,由于不兼容,所以该事务需要等待表锁操作的完成。
InnoDB 储存引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。支持两种意向锁:
-
意向共享锁(IS Lock),事务想要获得一张表中某几行的共享锁。
-
意向排他锁(IX Lock),事务想要获得一张表中某几行的排他锁。
由于 InnoDB 储存引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫描以外的任何请求。
表格:InnoDB 储存引擎中锁的兼容性
IS Lock IX Lock S Lock X Lock IS Lock 兼容 兼容 兼容 不兼容 IX Lock 兼容 兼容 不兼容 不兼容 S Lock 兼容 不兼容 兼容 不兼容 X Lock 不兼容 不兼容 不兼容 不兼容
4.一致性非锁定读、多版本并发控制
一致性的非锁定读(consistent nonlocking read)是指 InnoDB 储存引擎通过行多版本控制(multi versioning) 的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行 Delete 或者 UPDATE 操作,这时读取操作不会因此去等待行上锁的释放 。相反地,InnoDB 储存引擎会去读取行的一个快照数据。如图所示,之所以称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行的之前版本的数据,该实现是通过 undo 段来完成。而 undo 用来事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有事务需要对历史的数据进行修改操作。
可以看到,非锁定读机制极大地提高了数据库的并发性。在 InnoDB 储存引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是在每个事务隔离级别下都是采用非锁定的一致性读。此外,即使都是使用非锁定的一致性读,但是对于快照数据的定义也各不相同。从图可知,快照数据其实就是当前行数据之前的历史版本,每行记录可能有多个版本。一个行记录可能有不止一个快照数据,一般称这种技术为行多版本技术。由此带来的并发控制,称之为多版本并发控制(Multi Versioning Concurrency Control,MVCC)。
下面来通过 InnoDB 的简化版行为来说明 MVCC 是如何工作的。InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或者删除时间)。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在 REPEATABLE READ 的隔离级别下,MVCC 具体是如何操作的。
SELECT
InnoDB 储存引擎会根据以下两个条件检查每行记录:
a.InnoDB 储存引擎只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或者等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
b.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
只有符合上述两个条件的记录,才能返回作为查询结果。
INSERT
InnoDB 储存引擎为新插入的每一行保存当前系统版本号作为行版本号。
DELETE
InnoDB 储存引擎为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE
InnoDB 为插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原本的行作为行删除标识。
保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的储存空间,需要做更多的行检查工作,以及一些额外的维护工作。
MVCC 只在 REPEATABLE READ 和 READ COMMITTED 两个隔离级别下工作。其他两个隔离级别都和 MVCC 不兼容,因为 READ UNCOMMITTED 总是读取最新的数据行,而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
5.一致性锁定读
在默认配置下,即事务的隔离级别为 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,也是可以进行读取的。此外,SELECT ... FOR UPDATE、SELECT...LOCK IN SHARE MODE必须在同一个事务中,当事务提交了,锁也就释放了。因此在使用上述两句 SELECT 锁定语句时,务必加上 BEGIN,START TRANSACTION 或者 SET AUTOCOMMIT = 0。
6.锁的算法
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 对于行的查询都是采用这种锁定算法。通过这个算法解决了幻读(Phantom Problem)。
参考
- 《高性能MySQL》第一章
- 《MySQL技术内幕 InnoDB储存引擎》第六章