开发易忽视的问题:InnoDB 行锁设计与实现
开发易忽视的问题:InnoDB 行锁设计与实现
存储模型和锁机制
存储结构
-
数据页:
- InnoDB 将表的数据存储在数据页中,每个页默认大小为 16KB。
- 数据页中存储多个行记录,行记录按照主键顺序存放。
行格式:
- InnoDB 支持多种行格式,包括 Compact、Redundant、Dynamic 和 Compressed。这些格式影响数据的存储方式,但对行锁机制影响不大。
索引组织表:
- InnoDB 是一种索引组织表(Index-Organized Table),即数据按主键聚集存储,这意味着行数据实际上存储在 B+ 树的叶子节点上。
- 非主键索引以主键作为叶子节点指向的数据行的指针。
行锁实现
-
基于索引的锁定:
- 行级锁是基于索引实现的,具体来说,InnoDB 锁定的是索引上的记录,而不是实际的数据行。
- 当事务要锁定一行时,它实际上是在锁定该行所在索引的一个“间隙”或确切的索引键。
锁的类型:
- Record Lock:锁定单个索引记录。
- Gap Lock:锁定索引记录之间的间隙,用于防止幻影读,通常用于范围查询。
- Next-Key Lock:结合了 Record Lock 和 Gap Lock,在索引记录及其前面的间隙上加锁。这样可以防止其他事务插入新的索引记录。
锁的存储:
- 锁信息并不是直接存储在行本身,而是存储在内存中的锁表中。
- 锁表包含有关每个锁的信息,如锁的类型、被锁定的事务等。
压缩行锁:
- 为了减少锁的开销和提高效率,InnoDB 使用了一种称为锁压缩(Lock Compression)的技术,使得对于连续范围内的锁,仅需要维护较少数量的锁对象。
意向锁:
- 在表级别实现的一种锁,它表示某个事务想要在行级别获得锁。这种设计使得 InnoDB 能够快速判断是否可以安全地对整个表进行锁定。
实际操作
- 锁定机制依赖于索引,因此合理设计索引可以有效地利用行级锁。
- 当没有适当的索引来支持查询时,InnoDB 可能会退化到锁定更多的行甚至整个表。
锁表分析
- InnoDB 的锁表是用来管理和维护所有活动事务的锁信息的关键组件。锁表并不是一个物理存储的表,而是一种内存数据结构,其设计与实现旨在高效地处理并发事务,确保数据一致性和完整性。
锁表的设计和实现
-
锁结构:
- 每个锁都有一个锁结构,用于表示特定事务对某个资源(比如行或间隙)的锁定。
- 锁结构包含的信息包括:被锁定的资源、锁类型(如共享锁、排他锁)、拥有锁的事务ID等。
锁的存储:
- 锁信息保存在内存中,具体来说,是通过一个全局的
哈希表
来维护所有当前活动的锁。 - 这个哈希表以被锁定的资源为键,以相应的锁结构链表为值。这样可以快速查找某个资源上的锁。
锁类型:
- InnoDB 支持多种类型的锁,如前面提到的 Record Lock、Gap Lock 和 Next-Key Lock。每种锁类型在锁表中都有不同的表现形式和处理逻辑。
- 锁的类型决定了如何对其他事务进行阻塞或允许并发访问。
锁兼容性:
- 锁表需要处理锁的兼容性问题,即检测新请求的锁是否与现有锁冲突。共享锁与共享锁兼容,但排他锁则与其他任何锁冲突。
- 通过锁兼容矩阵,InnoDB 可以有效地决定是否授予新的锁请求。
死锁检测:
- InnoDB 实现了自动死锁检测机制,通过分析锁表中的
锁依赖图来判断是否存在死锁循环
。 - 一旦发现死锁,InnoDB 会主动回滚其中一个事务,以解除死锁状态。
锁等待队列:
- 对于因锁冲突而无法立即获得的锁请求,InnoDB 将其放入锁等待队列中。
- 当锁释放时,InnoDB 会检查锁等待队列,并尝试授予队列中合适的锁请求。
性能优化:
- 为了减少锁开销,InnoDB 使用了一些优化技术,比如锁压缩。这种技术在可能的情况下将多个相邻范围的锁合并成一个,从而减少锁对象的数量。
案例分析
-
假设我们有一个名为
employees
的表,其中包含如下字段:id
(主键),name
和salary
。该表的数据如下: -
现在我们有两个事务对这个表进行操作:
- 事务A:将 Bob 的薪水增加 1000。
- 事务B:尝试读取 Bob 和 Carol 的信息。
操作步骤及锁机制
-
事务A开始:
- 执行
START TRANSACTION;
- 执行
UPDATE employees SET salary = salary + 1000 WHERE name = 'Bob';
- 假如name字段没有添加索引,InnoDB 使用主键索引进行全表扫描,来定位 Bob 的行并获取行级锁(Record Lock)在 id=2 上,因为这是精确的行更新。
事务B开始:
- 执行
START TRANSACTION;
- 执行
SELECT * FROM employees WHERE name IN ('Bob', 'Carol') FOR UPDATE;
- 因为事务A已经持有了 id=2 的排他锁,事务B将在此处被阻塞,等待事务A释放对 Bob 的锁。
锁表存储与管理:
- 当事务A执行更新时,InnoDB 会在内存中的锁表中创建一个记录锁条目,标记该行(id=2)已被锁定,并且事务A拥有一个排他锁。
- 锁表的哈希结构会以 id=2 为键,指向一个链表,该链表包含事务A的锁信息。
- 当事务B尝试获取锁时,会检查锁表,发现冲突,因此会将其请求放入等待队列中,关联到相同的哈希键(即 id=2)。
事务A提交或回滚:
- 事务A完成后,执行
COMMIT;
或ROLLBACK;
。 - InnoDB 释放 id=2 的排他锁,从锁表中移除相应的锁条目。
- 此时,事务B会从等待队列中唤醒,重新尝试获取锁,现在它能够获取到 id=2 的共享锁或排他锁(视具体操作而定)。
事务B继续:
- 事务B成功获得锁后,可以读取 Bob 和 Carol 的信息。
- 完成后执行
COMMIT;
释放所有锁。
- 执行
结论
- 通过这个案例,我们可以看到 InnoDB 如何使用行级锁和锁表来管理并发访问。锁表是一个中间存储,用于跟踪当前所有活动事务的锁状态