MySQL 的加锁流程
文章目录
- MySQL 的加锁流程
- 简介
- 记录锁、间隙锁和临键锁概述
- 行级锁加锁流程
- 示例流程
- 两个原则,两个优化,一个 BUG
- 两个原则
- 两个优化
- 一个 BUG
- 示例1-主键(唯一)索引
- 示例 2-普通索引
- 总结
简介
在 MySQL InnoDB 存储引擎中,行级锁主要包括记录锁、间隙锁和临键锁。每种锁在不同的场景下用于确保数据的一致性和完整性。本文主要结合记录锁、间隙锁和临键锁来说明一下行级锁的加锁原理。
表级锁会直接锁表,一个事务加了锁,其余事务就要等待,所以表级锁不做过多介绍。
记录锁、间隙锁和临键锁概述
- 记录锁(Record Lock):锁定特定的索引记录。主要用于精确匹配的查询,防止其他事务修改该记录。
- 间隙锁(Gap Lock):锁定索引记录之间的间隙。用于防止其他事务在该间隙中插入新记录,从而避免幻读现象。
- 临键锁(Next-Key Lock):记录锁和间隙锁的组合。锁定一个索引记录及其前后的间隙,防止其他事务对这些范围内的数据进行插入、更新或删除操作。
行级锁加锁流程
在 InnoDB 中,加锁流程取决于查询类型和隔离级别。以下是结合记录锁、间隙锁和临键锁的行级锁加锁流程:
- 确定锁的类型和范围:
- 精确匹配查询(点查询):使用记录锁。例如,
SELECT * FROM table WHERE id = 1 FOR UPDATE
。 - 范围查询:使用临键锁。例如,
SELECT * FROM table WHERE id BETWEEN 1 AND 10 FOR UPDATE
。 - 唯一索引插入检查:可能使用间隙锁。
- 精确匹配查询(点查询):使用记录锁。例如,
- 开始事务:启动一个事务。
- 请求锁:
- 记录锁:如果查询是精确匹配查询,InnoDB 在对应的索引记录上加记录锁(X 锁或 S 锁)。
- 间隙锁:如果涉及唯一索引插入检查或需要防止幻读,InnoDB 在必要的间隙上加间隙锁。
- 临键锁:如果是范围查询,InnoDB 在查询范围内的每个索引记录及其前后的间隙上加临键锁。
- 检查锁的兼容性:
- 记录锁:检查目标记录是否已经被其他事务锁定。如果锁类型不兼容(例如,已有 X 锁),当前事务需等待。
- 间隙锁:检查目标间隙是否被其他事务锁定。如果已有间隙锁,且锁类型不兼容,当前事务需等待。
- 临键锁:检查目标范围内的索引记录及其前后的间隙是否被其他事务锁定。如果锁类型不兼容,当前事务需等待。
- 加锁:
- 成功通过锁的兼容性检查后,InnoDB 在目标记录或间隙上加锁。
- 更新 InnoDB 内部锁管理数据结构,记录锁的持有者和锁的类型。
- 执行查询或修改操作:在持有锁的情况下执行查询或修改操作。保证数据一致性。
- 提交或回滚事务:
- 提交事务:释放当前事务持有的所有锁。
- 回滚事务:撤销当前事务的所有修改,并释放所有锁。
示例流程
以下是一个详细的示例流程,假设我们有一个表 example
:
CREATE TABLE example (
id INT PRIMARY KEY,
value VARCHAR(50)
);
INSERT INTO example (id, value) VALUES (1, 'A'), (2, 'B'), (3, 'C');
场景 1:精确匹配查询
-
事务 A:
START TRANSACTION; SELECT * FROM example WHERE id = 1 FOR UPDATE; -- 对 id=1 加记录锁(X 锁)
-
事务 B:
START TRANSACTION; SELECT * FROM example WHERE id = 1 FOR UPDATE; -- 被阻塞,直到事务 A 提交或回滚
场景 2:范围查询
-
事务 A:
START TRANSACTION; SELECT * FROM example WHERE id BETWEEN 1 AND 3 FOR UPDATE; -- 对 id=1, id=2, id=3 及其间隙加临键锁
-
事务 B:
START TRANSACTION; INSERT INTO example (id, value) VALUES (2.5, 'D'); -- 被阻塞,直到事务 A 提交或回滚
场景 3:唯一索引插入检查
-
事务 A:
START TRANSACTION; INSERT INTO example (id, value) VALUES (4, 'D'); -- 插入成功,加间隙锁防止其他事务插入相同的 id
-
事务 B:
START TRANSACTION; INSERT INTO example (id, value) VALUES (4, 'E'); -- 被阻塞,直到事务 A 提交或回滚
两个原则,两个优化,一个 BUG
丁奇大佬在他的《MySQL 实战 45 讲》中总结了一些加锁规则,包含了两个原则,两个优化,一个 BUG。
两个原则
- 原则 1:加锁的基本单位是临键锁(Next-Key Lock),是一个左开右闭的区间。
- 原则 2:查找过程中访问到的对象才会加锁。
两个优化
- 优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
- 优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 BUG
- 唯一索引上的范围查询会访问到不满足条件的第一个值为止。
示例1-主键(唯一)索引
假如数据库表中有以下记录:
当我们执行 update t set d=d+1 where id=7
的时候,由于表 t
中没有 id=7
的记录,所以:
- 根据原则1,加锁单位是 next-key lock, session A 加锁范围就是 (5,10];
- 根据优化 2,这是一个等值查询 (id=7),而
id=10
不满足查询条件,next-key lock 退化成间隙锁,因此最终加锁的范围是(5,10)。
当我们执行 select * from t where id>=10 and id<11 for update
的时候:
- 根据原则1,加锁单位是 next-key lock,会给(5,10]加上 next-key lock,范围查找就往后继续找,找到
id=15
这一行停下来; - 根据优化 1,主键id 上的等值条件,退化成行锁,只加了
id=10
这一行的行锁。 - 根据原则 2,访问到的都要加锁,因此需要加 next-key lock(10,15]。因此最终加的是行锁
id=10
和 next- key lock(10,15]。
当我们执行 select *from t where id>10 and id<=15 for update
的时候:
- 根据原则 1,加锁单位是 next-key lock, 会给 (10,15] 加上 next-key lock, 并且因为 id 是唯一键,所以循环判断到
id=15
这一行就应该停止了。 - 但是,InnoDB 会往前扫描到第一个不满足条件的行为止,也就是
id=20
。而且由于这是个范围扫描,因此索引 id 上的(15,20]这个 next-key lock 也会被锁上。
示例 2-普通索引
假如数据库表中有以下记录:
当我们执行 select id from t where c=5 Lock in share mode
的时候:
- 根据原则1,加锁单位是 next-key lock,因此会给(0,5]加上 next-key lock。要注意c是普通索引,因此仅访问
c=5
这一条记录是不能马上停下来的,需要向右遍历,查到c=10
才放弃; - 根据原则 2,访问到的都要加锁,因此要给(5,10]加 next-key lock;
- 根据优化 2:等值判断,向右遍历,最后一个值不满足
c=5
这个等值条件,因此退化成间隙锁 (5,10); - 根据原则2,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁。
当我们执行 select * from t where c>=10 and c<11 for update
的时候:
- 根据原则1,加锁单位是 next-key lock,会给(5,10]加上 next-key lock,范围查找就往后继续找,找到
id=1,5
这一行停下来; - 根据原则 2,访问到的都要加锁,因此需要加 next-key lock(10,15];
- 由于索引 c 是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终 sesion A 加的锁是,索引上的(5,10]和(10,15] 这两个 next-key lock。
总结
MySQL 的 InnoDB 引擎下的 RR(Repeatable Read) 级别中,加锁的基本单位是临键锁(Next-Key Lock),只要扫描到的数据都会被加锁,唯一索引上的范围查询会访问到不满足条件的第一个为止。
通过记录锁、间隙锁和临键锁的配合,InnoDB 能够有效地管理并发事务,防止数据不一致和幻读现象。这些锁的加锁流程确保了在不同的查询和修改操作中,事务之间能够协调进行,提供了高效的并发控制和数据保护机制。在设计高并发系统时,理解和正确使用这些锁有助于提升系统的性能和可靠性。
引入这些锁,虽然一定程度上可以解决很多如幻读这样的问题,但是也会带来一些副作用,比如并发度降低和死锁等。当然,为了提升性能和并发度,也有两个优化点。