InnoDB - Lock
Created by : Mr Dk.
2020 / 10 / 17 20:20
Nanjing, Jiangsu, China
锁机制用于管理对共享资源的访问,使用户能够最大程度利用数据库的并发访问,同时又能以一致的方式读取和修改数据。
Lock in InnoDB
InnoDB 存储引擎会在行级别上对表数据进行加锁。InnoDB 存储引擎实现了两种标准行级锁:
- S Lock (共享锁,读锁) - 允许事务读一行数据
- X Lock (排他锁,写锁) - 允许事务删除或更新一行数据
行为与常见的读写锁一致:多个事务可以同时获取一行的读锁,但是只有获取一行写锁的事务可以修改该行数据。当事务 1 上的锁与事务 2 上的锁都不改变行数据时,称为 锁兼容;否则称为 锁不兼容。
S Lock | X Lock | |
---|---|---|
S Lock | 兼容 | 不兼容 |
X Lock | 不兼容 | 不兼容 |
InnoDB 还支持多粒度的锁定 - 允许事务在行级别上的锁和表级别上的锁同时存在。意向锁 (Intention Lock) 用于将锁定的对象分为多个层次,表示事务希望在更细的粒度上进行加锁:
- IS Lock (意向共享锁)
- IX Lock (意向排他锁)
如果事务要对某个页上的记录 r 上 X 锁,那么首先需要对数据库、表、页上 IX 锁,最后对记录上 X 锁。若其中任意一次上锁操作导致了等待 (不兼容),都需要等待粗粒度锁释放后再进一步上锁。规律总结如下:
- 意向锁之间兼容
- S Lock 之和 S/IS Lock 兼容
- X Lock 和其它所有锁都不兼容
IS Lock | IX Lock | S Lock | X Lock | |
---|---|---|---|---|
IS Lock | 兼容 | 兼容 | 兼容 | 不兼容 |
IX Lock | 兼容 | 兼容 | 不兼容 | 不兼容 |
S Lock | 兼容 | 不兼容 | 兼容 | 不兼容 |
X Lock | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
InnoDB 中,意向锁实际上就是表级别的锁,不会阻塞除全表扫描以外的任何请求。
Consistent Non-locking Read
一致性非锁定读 指 InnoDB 存储引擎通过管理多个版本的控制来读取数据库中的数据。假设当前读取的行正在进行 UPDATE
或 DELETE
操作,则不会因此去等待行上的 (X) 锁释放,而是直接读取行的 快照数据,从而避免了阻塞。快照数据是通过 undo 段实现的,undo 段本用于在事务中回滚数据,因此没有额外开销;另外,读取快照数据也不需要上锁。
每个行记录的快照可能会有多个版本,这就是所谓的行多版本并发控制 (Multi Version Concurrency Control, MVCC)。在不同的 事务隔离级别 下,使用的是不同版本的行快照。
READ COMMITTED
事务隔离级别下,读取行的最新快照数据 (其它事务对行的更改会被当前事务读取到,违反了 ACID 中的隔离性)REPEATABLE READ
事务隔离级别下,读取事务开始时的快照版本
Consistent Locking Read
在某些情况下,用户需要 显式 对数据库读取操作进行加锁来保证数据一致性。InnoDB 存储引擎对 SELECT
语句支持两种 一致性锁定读 操作:
SELECT ... FOR UPDATE
- 对读取的行记录加 X 锁SELECT ... LOCK IN SHARE MODE
- 对读取的行记录加 S 锁
上述两种语句必须出现在事务中。事务提交后,锁也就被释放了。
Auto Increment
InnoDB 存储引擎对每一个 含有自增长值 的表都带有一个自增长计数器,插入操作会根据计数器的值 + 1 赋予自增长列。该值可能会遭到并发修改,因此存在 AUTO-INC Locking,这是 MySQL 5.1.22 版本之前的实现方式。为了提高插入性能,锁不是在事务完成后才释放,而是完成执行插入自增长列的 SQL 语句后立刻释放该锁。
MySQL 后续支持了较为轻量级的自增长实现方式,即使用互斥量。但是可能带来的问题是每次插入时自增长的值可能不是连续的。
Lock Algorithm
行锁包含了三种算法:
- Record Lock - 单个行记录上锁
- Gap Lock - 锁定一个范围,但不锁定记录本身
- Next-Key Lock - 锁定一个范围,并锁定记录本身
当查询的索引中包含 唯一索引 时,InnoDB 存储引擎会将 Next-key Lock 降级为 Record Lock (因为没必要锁定一个范围),从而提高系统并发性。这些锁用于解决各种锁问题。
Phantom Problem
幻读 问题。在同一事务下,连续执行两次同样的 SQL 语句,得到的记录数不一样 (特指数据新增或删除)。比如在一个事务内执行了两次如下语句:
SELECT * FROM t WHERE a > 2 FOR UPDATE;
在该事务执行两次该语句之间,另一个事务插入了一个值为 4
的值,因此第二次执行该语句会得到不一样的结果。幻读违反了事务的隔离性。InnoDB 的 REPEATABLE READ
事务隔离等级下使用 Next-Key Locking 来解决幻读问题,即对 (2, +∞)
的范围加 X 锁,对该范围的任何插入都是不允许的。
Dirty Read
脏数据指事务对缓冲区中行记录修改但没有被提交的数据。如果一个事务读到了另一个事务中未提交的数据,就是脏读。脏读违反了数据库的隔离性,只有在 READ UNCOMMITTED
事务隔离等级下才可能会发生。
在修改事务时加 X 锁,当事务提交后再解锁,可以解决脏读问题。
Unrepeatable Read
不可重复读 指在一个事务内多次读取同一数据集合,同时另一事务也访问了同一数据集合并进行了 DML 操作 (并提交) - 第一个事务两次读取到的数据可能不一致 (特指数据更新)。由于不可重复读读到的是已经提交的数据,因此违反的是 ACID 中的一致性要求。
在 InnoDB 存储引擎中,还是通过 Next-Key Lock 算法来避免不可重复读的问题。在该算法下,对索引的扫描,不仅会锁住扫描到的索引,还会锁住索引的覆盖范围。实现该算法的事务隔离等级为 READ REPEATABLE
。
Lost Update
丢失更改 发生于一个事务的更新被另一个事务的更新覆盖。为了避免这个问题,需要让事务操作 串行化 - 在用户读取记录时上 X 锁,等事务提交后再释放 X 锁。
Dead Lock
死锁指的是两个或两个以上的事务在执行过程中,因争夺锁资源而造成的互相等待的现象。若无外力作用,则事务无法推进下去。解决死锁的方式是 超时 - 当一个事务等待时间超过阈值时,就让其中的一个事务回滚,另一个等待中的事务自然继续进行。但是根据 FIFO 的顺序选择事务进行回滚比较暴力,理论上应该选择 undo 代价较小的事务进行回滚。
当前数据库普遍使用 wait-for graph 进行死锁检测 - 构造对资源使用的图,如果图中存在回路,那么存在死锁。
Lock Escalation
锁升级 指的是当细粒度的锁数量过多,而锁本身是一种稀缺资源,那么可以将大量细粒度的锁升级为少量粗粒度的锁,从而保护了系统资源,防止系统使用过多内存来维护锁。对于 SQL Server 来说,由于锁是稀缺资源,锁升级会带来效率上的提高,但也可能导致并发性能的降低。而对于 InnoDB 存储引擎来说,由于不是根据每个记录产生锁,而是根据每个页对锁进行管理,因此不管事务锁住页中的几条记录,开销都是一致的。