深入剖析MySQL锁机制,多事务并发场景锁竞争
一、隐藏字段对 InnoDB 的行锁(Record Lock)与间隙锁(Gap Lock)的影响
1. 隐藏字段与锁的三大核心影响
类型 | 影响维度 | 描述 |
---|---|---|
DB_TRX_ID | MVCC 可见性控制 | 决定是否读取当前版本,或在加锁时避开不可见版本(影响加锁粒度) |
DB_ROLL_PTR | 构建多版本链 | 影响锁等待、加锁时的记录版本选择(间接决定是否锁冲突) |
隐式 RowID | 行定位与加锁 | 无主键时 RowID 作为唯一定位字段,对行锁加锁范围关键 |
2. DB_TRX_ID
与行锁冲突判断
✅ 场景:事务并发修改同一行数据
-
读取事务依据当前事务的
ReadView
,根据DB_TRX_ID
判断该版本是否“可见”。 -
写入事务加锁时,若当前行的
DB_TRX_ID
≠ 当前事务,则可能出现“当前锁冲突”或“等待前版本释放”。 -
若版本不可见,可能绕过该记录不加锁(如 RC 隔离级别下的 Next-Key Lock)
🎯 例子:RC 与 RR 加锁行为不同
-- T1: 开启事务,修改一行
BEGIN;
UPDATE user SET age = 30 WHERE id = 100;-- T2: 并发事务,尝试更新相同 id
BEGIN;
UPDATE user SET age = 35 WHERE id = 100; -- 被阻塞
解释:
-
InnoDB 通过
DB_TRX_ID
比较当前行的修改者是谁; -
若与当前事务不同 → 加锁或等待;
-
Read Committed 级别可能跳过不可见版本的加锁(幻读可能出现)。
3. DB_ROLL_PTR
与版本链上的加锁行为
✅ 场景:一行数据有多个历史版本
-
DB_ROLL_PTR
指向 undo log(之前的版本); -
在 RR 隔离级别下,InnoDB 可能根据
ReadView
沿着版本链找到合适版本读取; -
加锁时只锁当前版本(最新版本),但读取时可能读取旧版本。
🎯 幻读控制与间隙锁相关:
-- T1: 查询 WHERE age > 30
-- T2: 在该范围内插入新记录
-- T1: 再次查询,同样语句,发现多了新记录 → 幻读
为了防止 T2 插入“未来可能匹配”的记录,InnoDB 使用
Gap Lock
或Next-Key Lock
对间隙加锁。
是否加锁哪些行/间隙,DB_ROLL_PTR
决定了当前记录是否属于可见范围 → 是否参与锁计算。
4. 隐式 RowID 与加锁定位(Record Lock)
✅ 场景:无主键表
CREATE TABLE t (name VARCHAR(100)
) ENGINE=InnoDB;
-
InnoDB 自动创建一个 6 字节 RowID(隐式主键)
-
聚簇索引以 RowID 为 key 建树
-
所有辅助索引也指向 RowID
🔐 加锁行为:
-
对于无主键表,InnoDB 通过 RowID 精确加锁;
-
行锁定位依赖 RowID;
-
辅助索引加锁回表时,也依赖 RowID 判断目标行;
📌 所以:如果你不建主键,行锁仍然是可精确的,但依赖的是隐式 RowID
5. 隐藏字段如何影响三种锁类型
锁类型 | 是否依赖隐藏字段 | 说明 |
---|---|---|
行锁(Record) | ✅ RowID , DB_TRX_ID | 判断是否锁冲突、锁定位 |
间隙锁(Gap) | ✅ DB_TRX_ID , ROLL_PTR | 是否跳过某些版本,加锁哪些间隙 |
Next-Key Lock | ✅ 混合依赖 | 加锁实际记录+其间隙 |
6. 实战举例:两个事务交错更新记录
-- T1
BEGIN;
SELECT * FROM user WHERE age > 30 FOR UPDATE;-- T2
BEGIN;
INSERT INTO user(id, name, age) VALUES (5, 'Mike', 35);
🔍 解析:
-
T1 通过聚簇索引扫描记录,遇到每一行:
-
检查
DB_TRX_ID
,判断是否可见; -
依据可见版本决定加锁(Next-Key Lock → 行+间隙);
-
-
T2 插入时,必须检测新记录是否在 T1 加锁区间内 → 若是则阻塞;
即便某一行在 T1 的快照中不可见,但只要它是当前版本,T1 可能仍然加锁(受隔离级别控制)
7. 隐藏字段在锁机制中的作用
隐藏字段 | 对锁的影响 |
---|---|
DB_TRX_ID | 决定事务是否看到当前版本 → 影响加锁行为与锁冲突判定 |
ROLL_PTR | 构造历史版本链,决定 MVCC 可见性 → 影响是否需要加锁 |
Row ID | 无主键表唯一标识 → 用于加锁定位、辅助索引回表行锁 |
RecordHeader | 是否删除标志 → 已删除记录是否参与加锁由此决定 |
二、深入剖析行级锁和间隙锁
主要将深入剖析:
-
✅ 各种锁类型的定义与底层机制
-
✅ 锁的触发场景与加锁策略
-
✅ InnoDB 加锁流程与隐藏字段的关系
-
✅ 常见加锁案例分析(如幻读、唯一键冲突)
-
✅ 可视化锁冲突与调试技巧
1. InnoDB 锁类型总览
锁类型 | 粒度 | 描述 |
---|---|---|
✅ 行锁(Record Lock) | 精确锁住一行记录(聚簇索引记录) | |
✅ 间隙锁(Gap Lock) | 锁住两个索引记录之间的“间隙”,不含记录本身 | |
✅ Next-Key Lock | 行锁 + 间隙锁,锁住记录及其前后间隙 | |
🔄 插入意向锁(Insert Intention Lock) | 特殊锁 | 标记插入意图,不是互斥锁,但参与死锁检测 |
2. 行锁(Record Lock)
📌 定义:
锁定聚簇索引中的一条具体记录。
🔧 加锁条件:
-
明确通过 主键 / 唯一键 精确定位某条记录;
-
触发语句通常为:
SELECT * FROM t WHERE id = 1 FOR UPDATE; UPDATE t SET name = 'x' WHERE id = 1;
🧬 底层机制:
-
锁记录基于 B+ 树中记录的物理位置;
-
锁信息存储在 锁数据结构 lock_t 中,并挂载到事务事务结构 trx_t 的锁链表。
3. 间隙锁(Gap Lock)
📌 定义:
锁住两条索引记录之间的范围(gap),但不包括已有的记录。
🔧 加锁场景:
-
防止幻读:防止其他事务在该范围内插入新记录;
-
RR
(Repeatable Read)下执行范围条件的SELECT ... FOR UPDATE
、DELETE
、UPDATE
; -
示例:
-- 假设表中已有 id = 100, 200
SELECT * FROM t WHERE id > 100 AND id < 200 FOR UPDATE;
-- 锁定的是 (100, 200) 的间隙,不包括100和200
🧬 底层机制:
-
锁住 B+ 树中的两个键值之间的指针区域;
-
无具体记录,但会在锁表中以特殊“GAP”标志表示。
4. Next-Key Lock(默认使用)
📌 定义:
Next-Key Lock = Record Lock + Gap Lock
即锁住 记录本身 + 其前面的间隙
🔧 加锁场景:
-
默认隔离级别为 RR(可重复读) 时,InnoDB 对范围查询使用 Next-Key Lock;
-
作用:
-
防止幻读(新插入记录“幻出现”)
-
保证范围读一致性
-
🌰 举例:
-- 表中已有 id = 100, 200
SELECT * FROM t WHERE id >= 100 AND id < 200 FOR UPDATE;
此时锁住范围:
-
间隙 (100, 200)
-
记录 id = 100
5. 锁的触发机制
1️⃣ 锁的决定因素
影响项 | 描述 |
---|---|
SQL 类型 | SELECT ... FOR UPDATE / DELETE / UPDATE 会加锁 |
隔离级别 | RR 使用 Next-Key Lock,RC 只锁记录本身 |
访问条件 | 主键精确命中加 Record Lock,范围条件加 Gap Lock/Next-Key Lock |
是否命中索引 | 走索引加行锁;走全表扫描加表锁 |
2️⃣ 加锁时机
-
执行语句解析完成、访问 B+ 树查找记录时;
-
遇到匹配记录时,根据事务隔离级别加锁;
-
加锁时会检查
DB_TRX_ID
,判断该版本是否对当前事务可见;-
若不可见(由其他事务正在修改),可能等待或加锁历史版本(undo 构建视图)
-
6. 锁冲突案例剖析
📍 幻读问题(RR下通过 Gap Lock/Next-Key Lock 解决)
-- T1
BEGIN;
SELECT * FROM t WHERE age > 30 FOR UPDATE;-- T2
INSERT INTO t(age) VALUES (35); -- 被阻塞(因 T1 已加锁间隙)
📍 唯一键冲突(加锁 + 意向锁)
-- T1
INSERT INTO t(id, name) VALUES (100, 'A');-- T2
INSERT INTO t(id, name) VALUES (100, 'B'); -- 被阻塞(同一主键)
📍 死锁触发
-- T1
UPDATE t SET name = 'A' WHERE id = 1;-- T2
UPDATE t SET name = 'B' WHERE id = 2;-- 然后相互更新对方的记录,将触发死锁
7. 加锁调试技巧
✅ 查看当前锁情况:
SELECT * FROM information_schema.innodb_locks;
SELECT * FROM performance_schema.data_locks;
✅ 死锁日志:
SHOW ENGINE INNODB STATUS \G
可定位谁等待谁、加了什么锁、是否超时或死锁。
8. 锁类型与机制全图
┌────────────────────┐│ SQL 语句类型 │└────────────────────┘│┌────────▼────────┐│ 是否走索引? │──否──▶ 表锁(意外情况)└────────┬────────┘│是┌────────▼────────┐│ 锁定条件类型 │└────────┬────────┘精确匹配 │ 范围匹配│ ▼┌──────▼──────┐ ┌────────────┐│ 行锁 │ │ Next-Key 锁 │└─────────────┘ └────────────┘↑ ↑RC 可降为 Record Lock RR 加间隙锁避免幻读
三、深入剖析 MySQL InnoDB 中多事务并发场景下的锁竞争与回滚机制
1. 核心概念概览
概念 | 描述 |
---|---|
锁竞争 | 多个事务试图访问同一资源(记录/间隙)但互斥,形成等待或死锁 |
回滚机制 | 事务执行失败、冲突或死锁时,撤销已执行部分操作,恢复一致状态 |
死锁检测与回滚策略 | InnoDB 采用 Wait-for Graph 检测死锁,选择某个事务回滚释放锁 |
2. 典型并发冲突与回滚案例
📍 案例 1:更新相同记录引发锁等待
表结构:
CREATE TABLE account (id INT PRIMARY KEY,balance INT
) ENGINE=InnoDB;
INSERT INTO account VALUES (1, 1000), (2, 2000);
并发场景:
-- Session A
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;-- Session B
START TRANSACTION;
UPDATE account SET balance = balance + 100 WHERE id = 1; -- 被阻塞
🔍 分析:
-
id=1
被 Session A 先锁定(行锁)。 -
Session B 尝试更新同一记录,被阻塞。
-
若 A 提交或回滚,B 才能继续执行。
📍 案例 2:幻读冲突引发间隙锁竞争(RR隔离)
-- Session A
START TRANSACTION;
SELECT * FROM account WHERE id BETWEEN 1 AND 3 FOR UPDATE;-- Session B
INSERT INTO account VALUES (3, 3000); -- 阻塞
🔍 分析:
-
A 加了 Next-Key Lock:锁定了 id=1 和 id=2 以及间隙 (2,∞)。
-
插入 id=3 的操作冲突于间隙锁,Session B 阻塞。
-
A 提交或回滚后,B 才能插入。
📍 案例 3:死锁发生,InnoDB 检测并回滚
-- Session A
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;-- Session B
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 2;-- Session A
UPDATE account SET balance = balance + 100 WHERE id = 2; -- 阻塞-- Session B
UPDATE account SET balance = balance + 100 WHERE id = 1; -- 死锁
🔍 死锁图:
A 等待 B 释放 id=2
B 等待 A 释放 id=1
⇒ 形成死锁
💥 InnoDB 处理机制:
-
InnoDB 启动“死锁检测器”,构建 Wait-for Graph;
-
选择一个开销更小的事务(通常是等待时间短的),执行自动回滚;
-
抛出错误:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
3. InnoDB 回滚机制详解
✅ 回滚触发点:
-
显式
ROLLBACK
-
死锁检测回滚事务
-
唯一键/外键冲突
-
DDL 失败隐式回滚
✅ 回滚操作步骤:
-
利用隐藏字段
DB_ROLL_PTR
回溯 Undo Log 链; -
撤销所有已变更记录(覆盖旧值);
-
释放已加锁资源(行锁、间隙锁);
-
标记事务为
ROLLING BACK
状态;
✅ 相关结构:
结构 | 描述 |
---|---|
Undo Log | 存储旧版本数据用于回滚与 MVCC |
TRX结构体 | 保存事务状态及锁信息链表 |
Lock Hash Table | 管理锁持有和等待信息 |
4. 锁冲突 & 回滚实验演示命令
🔧 查看当前锁持有状态
-- 查看当前锁
SELECT * FROM information_schema.innodb_locks;-- 查看锁等待关系
SELECT * FROM information_schema.innodb_lock_waits;
🔧 查看死锁日志
SHOW ENGINE INNODB STATUS\G;
查看最新一次死锁信息、涉及记录、被回滚的事务等。
5. 最佳实践建议
场景 | 建议 |
---|---|
多事务更新热点记录 | 使用悲观锁 + 乐观重试机制,避免死锁 |
范围锁定 | 使用主键精准定位,减少间隙锁 |
防止死锁 | 保证事务更新顺序一致,如永远先更新 id小的记录 |
并发冲突排查 | 使用 innodb_status + performance_schema.data_locks 分析锁链和回滚 |
6. 总结
关键点 | 内容 |
---|---|
锁竞争 | 由事务访问冲突资源引发,可能阻塞 |
回滚 | 自动或手动撤销事务操作,使用 Undo 日志还原 |
死锁检测 | InnoDB 内部维护等待图,自动检测并终止代价小的事务 |
调试工具 | information_schema 、SHOW ENGINE INNODB STATUS 、慢查询日志等 |