深入剖析MySQL锁机制,多事务并发场景锁竞争

一、隐藏字段对 InnoDB 的行锁(Record Lock)与间隙锁(Gap Lock)的影响


1. 隐藏字段与锁的三大核心影响

类型 影响维度 描述
DB_TRX_ID MVCC 可见性控制 决定是否读取当前版本,或在加锁时避开不可见版本(影响加锁粒度)
DB_ROLL_PTR 构建多版本链 影响锁等待、加锁时的记录版本选择(间接决定是否锁冲突)
隐式 RowID 行定位与加锁 无主键时 RowID 作为唯一定位字段,对行锁加锁范围关键

2. DB_TRX_ID 与行锁冲突判断

✅ 场景:事务并发修改同一行数据

  1. 读取事务 依据当前事务的 ReadView,根据 DB_TRX_ID 判断该版本是否"可见"。

  2. 写入事务加锁时 ,若当前行的 DB_TRX_ID ≠ 当前事务,则可能出现"当前锁冲突"或"等待前版本释放"。

  3. 若版本不可见,可能绕过该记录不加锁(如 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 LockNext-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 是否删除标志 → 已删除记录是否参与加锁由此决定

二、深入剖析行级锁和间隙锁

主要将深入剖析:

  1. ✅ 各种锁类型的定义与底层机制

  2. ✅ 锁的触发场景与加锁策略

  3. ✅ InnoDB 加锁流程与隐藏字段的关系

  4. ✅ 常见加锁案例分析(如幻读、唯一键冲突)

  5. ✅ 可视化锁冲突与调试技巧


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 UPDATEDELETEUPDATE

  • 示例:

    -- 假设表中已有 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 回滚机制详解

✅ 回滚触发点:

  1. 显式 ROLLBACK

  2. 死锁检测回滚事务

  3. 唯一键/外键冲突

  4. DDL 失败隐式回滚

✅ 回滚操作步骤:

  1. 利用隐藏字段 DB_ROLL_PTR 回溯 Undo Log 链;

  2. 撤销所有已变更记录(覆盖旧值);

  3. 释放已加锁资源(行锁、间隙锁);

  4. 标记事务为 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_schemaSHOW ENGINE INNODB STATUS、慢查询日志等
相关推荐
Zfox_4 小时前
Redis:Hash数据类型
服务器·数据库·redis·缓存·微服务·哈希算法
陈丹阳(滁州学院)6 小时前
若依添加添加监听容器配置(删除键,键过期)
数据库·oracle
远方16097 小时前
14-Oracle 23ai Vector Search 向量索引和混合索引-实操
数据库·ai·oracle
GUIQU.8 小时前
【Oracle】数据仓库
数据库·oracle
恰薯条的屑海鸥8 小时前
零基础在实践中学习网络安全-皮卡丘靶场(第十六期-SSRF模块)
数据库·学习·安全·web安全·渗透测试·网络安全学习
咖啡啡不加糖8 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存
曼汐 .8 小时前
数据库管理与高可用-MySQL高可用
数据库·mysql
MickeyCV8 小时前
使用Docker部署MySQL&Redis容器与常见命令
redis·mysql·docker·容器·wsl·镜像
2301_793102498 小时前
Linux——MySql数据库
linux·数据库
喵叔哟8 小时前
第4章:Cypher查询语言基础
数据库