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
。该表的数据如下:
id | name | salary |
---|---|---|
1 | Alice | 5000 |
2 | Bob | 6000 |
3 | Carol | 7000 |
现在我们有两个事务对这个表进行操作:
事务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 如何使用行级锁和锁表来管理并发访问。锁表是一个中间存储,用于跟踪当前所有活动事务的锁状态
思考题:查询条件没有索引,一定会进行表锁嘛
如果在执行 SELECT * FROM employees WHERE name = 'Alice' FOR UPDATE;
时,name
字段上没有索引,那么 InnoDB 会执行全表扫描来查找满足条件的记录。这时,InnoDB 的锁机制会有所不同:
全表扫描:由于缺少索引,InnoDB 必须扫描整张表来找到所有符合条件的行。
行锁应用:即使是在全表扫描的情况下,InnoDB 仍会尝试对每一行进行行级锁(Record Lock)。不过,由于需要检查每一行,效率会降低。
锁定性能影响:在没有索引的情况下,锁定的范围可能会更广泛,因为必须访问每个数据页和其中的每一行来判断是否符合条件。这可能导致更多的锁竞争和降低并发性能。
优化建议:
为避免这种低效的操作,建议为常用的查询条件列添加适当的索引。
添加索引不仅可以提高查询的性能,还能使锁更精确,从而提高事务的并发处理能力。