当前位置: 首页 > news >正文

深入剖析MYSQL MVCC多版本并发控制+ReadView视图快照规避幻读问题

一、剖析MVCC底层如何实现


1. MVCC 的目标

在高并发场景下,MVCC 实现:

能力描述
读写并发读不会阻塞写,写也不会阻塞读(大多数 SELECT 是非阻塞的)
一致性视图每个事务看到的数据是其开始时的一致状态
高性能避免频繁加锁,减少锁竞争开销

2. InnoDB 如何实现 MVCC

InnoDB 使用以下两部分实现 MVCC:

  1. Undo Log(撤销日志):用于保存数据变更前的旧版本

  2. ReadView(读视图):用于判断当前事务“能不能看到”某一行的某个版本


1. Undo Log:保存老版本数据

每当事务修改一条记录,InnoDB 会把修改前的值记录在 Undo Log 中:

UPDATE user SET age = 30 WHERE id = 1;
-- Undo Log: 保存 age = 20 的旧值

Undo Log 是行级的、链式结构,连接着该记录的所有历史版本,形成“版本链”。

每条数据记录会在内部隐藏字段中存储:

字段作用
DB_TRX_ID最后修改该记录的事务 ID(说白了,当前版本属于哪个事务)
DB_ROLL_PTR回指 undo log 的指针,可沿指针“找老版本”
DB_ROW_ID(隐藏主键)若无主键则生成的自增 ID

示例:记录的多版本链

记录原始状态:age = 20

UPDATE age = 30  => Undo1: age = 20, trx_id=100UPDATE age = 40  => Undo2: age = 30, trx_id=101Undo1: age = 20, trx_id=100

记录链如下:

最新版本
┌──────────────┐
│ age = 40     │ trx_id = 102
│ roll_ptr → Undo2
└──────────────┘↓
Undo2: age=30, trx_id=101↓
Undo1: age=20, trx_id=100

2. ReadView:事务的可见性判断标准

当一个事务开始读取数据,InnoDB 会创建一个 ReadView(快照),它决定“哪些版本是我能看到的”。

ReadView 包含内容:
字段含义
m_ids当前活跃事务列表(未提交事务的 trx_id)
min_trx_id活跃事务中最小的事务 ID
creator_trx_id当前事务的 ID
up_limit_id下限:min(m_ids)
low_limit_id上限:最大事务 ID(+1)

3. 可见性判断逻辑:

当事务 A 在 ReadView 中读取某条记录,InnoDB 按以下条件判断该记录版本是否可见:

X 是记录的 DB_TRX_ID,表示这条记录是哪个事务写的:

条件是否可见含义
X == 当前事务 ID✅ 可见当前事务自己写的数据
X < min_trx_id✅ 可见提交时间早于当前快照
X ∈ 活跃事务列表 m_ids❌ 不可见还未提交的事务,不能看到
X > low_limit_id❌ 不可见新事务,还没提交,不能看到

💡 若不满足可见性,InnoDB 会沿着 roll_ptr 追溯旧版本,直到找到符合 ReadView 的版本为止。


3. 完整示例(含快照机制)

-- 假设当前数据:
id = 1, age = 20, trx_id = 100-- 事务A:
START TRANSACTION;    -- trx_id = 101
SELECT age FROM user WHERE id = 1;  -- ReadView 创建-- 事务B:
START TRANSACTION;    -- trx_id = 102
UPDATE user SET age = 30 WHERE id = 1;
COMMIT;-- 事务A再次读取:
SELECT age FROM user WHERE id = 1;

✅ 事务A两次读取都看到 age = 20,因为 ReadView 不包含事务102提交后的版本。


4. MVCC 的限制

限制项描述
只适用于 SELECTDELETE, UPDATE, INSERT 仍然加锁
仅限 RC/RR 隔离级别READ UNCOMMITTED 不使用 MVCC,SERIALIZABLE 强制加锁
范围加锁仍需要间隙锁并不能防止幻读,仍需 SELECT ... FOR UPDATE 配合
Undo 不是永久保存被 purge 线程清理后,不可回溯旧版本

5. MVCC 工作流程图

             事务A                事务B│                    │┌────────────▼────────────────────▼────────────┐│           InnoDB 引擎层                      ││                                              ││   +-------------------------------+          ││   | 当前记录(最新版本)         |          ││   | DB_TRX_ID = 102              | ← 写入   ││   | DB_ROLL_PTR → Undo2          |          ││   +-------------------------------+          ││            ↓                                  ││      Undo2(trx_id=101) ← A 可见             ││            ↓                                  ││      Undo1(trx_id=100)                      │└──────────────────────────────────────────────┘A 使用 ReadView(trx_id=101),只看见 trx_id ≤ 100 的版本

6. 如何查看事务信息 / ReadView

-- 查看当前事务
SELECT * FROM information_schema.innodb_trx;-- 查看 undo 使用情况(8.0+)
SELECT * FROM performance_schema.data_locks;

7. 总结

组件作用
Undo Log存储旧版本,用于回滚与版本回溯
ReadView事务视图,决定“你能看到谁的版本”
DB_TRX_ID表示记录是哪个事务写入的
DB_ROLL_PTR指向 undo,构成多版本链
可见性判断是否在 ReadView 中“可见” = 是否为可读历史版本

8. 建议实践

场景建议做法
高并发下的数据一致性需求开启 REPEATABLE READ,使用 MVCC
需要防止幻读搭配 SELECT ... FOR UPDATE 使用
查询长时间读旧数据合理配置 purge 线程参数,保留 undo
手动事务控制显式 START TRANSACTION 控制 ReadView 生命周期

二、深入剖析InnoDB可见性算法和 ReadView 构建机制

1. 可见性算法:解决什么问题

在多事务并发执行中:

  • 同一条记录可能被多个事务读写;

  • 每个事务只能看到符合自己视图的数据版本(避免读未提交/幻读等);

  • InnoDB 使用 MVCC + 可见性算法 决定你是否能“看见”一条记录的某个版本。

这个过程的核心就是:判断某个版本(记录的 trx_id)是否对当前事务可见?


2. 可见性算法核心逻辑(重点)

每条记录的当前版本都带有一个隐藏字段:

  • DB_TRX_ID:最后修改该记录的事务 ID(也就是该版本由哪个事务创建)

  • DB_ROLL_PTR:指向 undo log 的指针,用于回溯历史版本

每个事务读取数据时,会使用当前事务生成的 ReadView 来判断版本可见性。

可见性判断规则(InnoDB):

设:

  • TRX_ID:这条记录的版本是由哪个事务创建的;

  • trx_id:当前事务的 ID;

  • ReadView.m_ids:当前 ReadView 中的“活跃事务列表”(未提交);

  • up_limit_id = 最小活跃事务 ID;

  • low_limit_id = 当前系统中尚未分配的最小可用事务 ID;

可见性判断逻辑如下:
条件是否可见含义
TRX_ID == trx_id✅ 是当前事务自己创建的版本(最新)
TRX_ID < up_limit_id✅ 是创建者事务早于当前快照创建,且已提交
TRX_ID ∈ ReadView.m_ids❌ 否创建者事务未提交,不可见
TRX_ID ≥ low_limit_id❌ 否创建者事务尚未开始(并发之后的事务),不可见

3. ReadView 是什么?如何构建?

ReadView 是一个结构体,它描述了某个时刻所有活跃事务的快照,由 InnoDB 在以下场景中构建:

✅ 构建时机:

  • 第一次执行 SELECT(非锁定读取)时自动创建;

  • 若显式开启事务:START TRANSACTION + 第一次查询时构建;

  • 若自动提交模式:每条 SELECT 都独立构建一个新的 ReadView。


✅ ReadView 包含的内容:

字段含义
creator_trx_id当前事务 ID
up_limit_id活跃事务中最小的事务 ID
low_limit_id当前系统中尚未分配的最小事务 ID(即最大已分配+1)
m_ids[]当前所有活跃事务的 ID 列表(未提交的)

这个结构决定了“哪些事务写的版本对我可见”。


✅ 构建流程:

  1. 当前事务在 trx_sys 中注册(分配 trx_id);

  2. 遍历 trx_sys 全部事务列表,获取所有已开启但未提交事务,生成 m_ids[]

  3. 计算出 up_limit_id = min(m_ids)low_limit_id = 最大已分配事务ID + 1

  4. ReadView 绑定到当前事务的上下文;

  5. 后续查询使用此 ReadView 判断版本可见性。


4. 可见性判断完整流程图解

        +---------------------+|   当前事务 trx_id   |+---------------------+|第一次执行 SELECT↓+---------------------+|   构建 ReadView     ||---------------------|| m_ids: [101, 102]   | ← 当前活跃事务ID| creator_id: 103     || up_limit_id: 101    || low_limit_id: 105   |+---------------------+↓遍历记录版本链(每条记录)↓对于记录 R,版本由 trx_id = X 创建:判断是否可见:┌────────────┬──────────────────────┐│ 条件        │ 是否满足             │├────────────┼──────────────────────┤│ X == 103    │ 当前事务写的版本     │ ✅│ X < 101     │ 老事务,已提交       │ ✅│ X ∈ m_ids[] │ 活跃未提交事务写的版本│ ❌│ X ≥ 105     │ 新事务,未开始       │ ❌└────────────┴──────────────────────┘

5. 可见性算法的代码层实现简析(源码级别)

InnoDB 实现版本可见性的核心代码在文件 row0sel.cc 中,核心函数为:

row_search_mvcc()

内部调用了:

row_sel_build_read_view_if_needed()
trx_is_active()
row_vers_build_for_consistent_read()
row_vers_old_has_index_entry()

可见性判断核心函数是:

row_vers_is_version_consistent()

它判断给定记录版本 trx_id 是否落在当前事务的 ReadView 可见范围内。


6. 实操观察:如何看到 ReadView / 事务状态

查询当前活跃事务(生成 ReadView 基础):

SELECT * FROM information_schema.innodb_trx;

输出字段包括:

  • trx_id

  • trx_started

  • trx_state

  • trx_query

查看当前版本使用的 undo 和锁状态:

SELECT * FROM performance_schema.data_locks;

7. ReadView 与隔离级别的关系

隔离级别ReadView 特性MVCC 是否启用
READ UNCOMMITTED不创建 ReadView
READ COMMITTED每次查询生成新的 ReadView
REPEATABLE READ第一次查询创建,整个事务复用
SERIALIZABLE强制加锁,不依赖 MVCC❌(或部分)

8. 总结

组件描述
DB_TRX_ID记录的创建者事务 ID
DB_ROLL_PTR指向旧版本 undo 链
ReadView当前事务能看到哪些事务写的版本
判断逻辑本事务写的、已提交、比我老 → ✅可见
作用场景非锁定读取(如 SELECT)使用可见性判断,不加锁

三、事务交叉并发操作导致事务视图冲突的场景

1. 环境准备

表结构:

CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(20),age INT
) ENGINE=InnoDB;

初始数据:

INSERT INTO user (id, name, age) VALUES (1, 'Alice', 20);

设置隔离级别为:REPEATABLE READ(InnoDB 默认)

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

2. 事务交叉并发场景演练

场景 1:事务A看不到事务B提交的数据(MVCC 快照生效)

🧪 目标:验证事务A使用的是旧快照(ReadView)

-- 会话A:
START TRANSACTION;
SELECT age FROM user WHERE id = 1;  -- 读取 age = 20(创建 ReadView)-- 会话B:
START TRANSACTION;
UPDATE user SET age = 30 WHERE id = 1;
COMMIT;-- 会话A(再次读取):
SELECT age FROM user WHERE id = 1;  -- age = 20(依旧使用 ReadView)
COMMIT;

✅ 解释:

  • A 的 ReadViewSTART TRANSACTION + SELECT 时创建;

  • B 的更新提交后虽然已经生效;

  • 但 A 一直用的是旧快照,因此只能看到原值 20

  • MVCC 生效,读一致性保证。


场景 2:事务A更新数据后立即读取到新值(自己可见)

🧪 目标:验证事务内部写入对自己始终可见

-- 会话A:
START TRANSACTION;
UPDATE user SET age = 40 WHERE id = 1;  -- 修改成功
SELECT age FROM user WHERE id = 1;      -- age = 40 ✅
COMMIT;

✅ 解释:

  • InnoDB 可见性规则中,当前事务自己创建的版本 一定可见;

  • 不管 ReadView 怎么设置,DB_TRX_ID == 当前事务ID → 总是可见;

  • 所以事务中写自己、读自己,肯定能看到最新的值。


场景 3:事务B无法看到A未提交的数据(读已提交验证)

🧪 目标:读未提交/读已提交隔离级别对可见性的影响

设置:
-- 会话B设置为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

执行流程:

-- 会话A
START TRANSACTION;
UPDATE user SET age = 50 WHERE id = 1;-- 会话B
START TRANSACTION;
SELECT age FROM user WHERE id = 1;  -- 看到的是 40,A未提交
COMMIT;-- 会话A 提交
COMMIT;

✅ 解释:

  • A 修改后的新版本未提交,对其他事务不可见;

  • B 作为 READ COMMITTED,只能看到已提交版本

  • 所以仍然读到提交前的 age = 40

  • 实现了避免脏读的目标


场景 4:事务A插入新记录,事务B看不到(可见性 + 幻读验证)

🧪 目标:验证 ReadView 快照不能看见并发 insert

-- 会话A:
START TRANSACTION;
SELECT * FROM user WHERE age > 10;  -- 看到 id=1 一条记录-- 会话B:
START TRANSACTION;
INSERT INTO user (id, name, age) VALUES (2, 'Bob', 25);
COMMIT;-- 会话A(再次读取):
SELECT * FROM user WHERE age > 10;  -- 仍然只有 id=1 ✅
COMMIT;

✅ 解释:

  • 会话 A 的 ReadView 不包括 B 的事务;

  • 虽然 B 成功插入数据,但 A 的快照中根本“没这个事务”;

  • 所以 A 看不到插入的新行,即使它“符合条件”;

  • 这就是 MVCC 避免幻读的前提(非锁定读)。


场景 5:强制加锁,阻止幻读插入

🧪 目标:使用 SELECT ... FOR UPDATE 强制锁住间隙,阻止其他事务插入

-- 会话A:
START TRANSACTION;
SELECT * FROM user WHERE age > 10 FOR UPDATE;  -- 加 next-key lock(间隙锁)-- 会话B:
START TRANSACTION;
INSERT INTO user (id, name, age) VALUES (3, 'Carl', 22);  -- 🚫 阻塞 ❗

✅ 解释:

  • 会话 A 加了 Next-Key Lock(行锁 + 间隙锁),锁住 age > 10 范围;

  • B 插入的 age=22 落在该间隙中,被锁;

  • 真正避免了幻读(防止新行插入)

  • 这是防幻读最彻底的手段,比 MVCC 本身更强。


3. 事务视图冲突本质总结

冲突情况是否可见原因
当前事务自己创建的记录✅ 可见DB_TRX_ID == trx_id
已提交事务创建的版本,在视图之前提交✅ 可见MVCC 快照可见
并发事务创建的新版本❌ 不可见在 ReadView 中不可见
已提交的新事务写入,但 ReadView 早于它❌ 不可见快照不可见
幻读插入记录❌(MVCC) / 🚫(加锁)MVCC 屏蔽 / Next-Key Lock 阻止

4. 实操建议与最佳实践

场景建议
跨多次查询要保证数据一致显式 START TRANSACTION,复用 ReadView
并发写入读操作多用 MVCC + REPEATABLE READ
精确防幻读使用 SELECT ... FOR UPDATE 锁定间隙
高并发插入/更新监控锁冲突,避免大范围查询加锁
实时读取最新数据使用 READ COMMITTED 隔离级别

四、深入剖析 MySQL InnoDB 如何从底层解决幻读

我们需要从两个核心机制入手:

MVCC(多版本并发控制) – 避免“读到别人新插入的数据”
间隙锁(Gap Lock) + Next-Key Lock – 阻止别人“插入我看不见的新数据”


1. 什么是幻读(Phantom Read)

幻读:在同一个事务中,两次相同条件的查询,返回的结果集不一样,而这个差异来自于其他事务新增的“符合条件”的行

📍 示例:

-- 第一次查询
SELECT * FROM orders WHERE price > 100;-- 另一个事务插入一条新记录(price=200)-- 第二次查询,结果多了一行

这种“新冒出来的记录”,就叫“幻影行” —— 这就是幻读


2. InnoDB 的两种防幻方式

手段能否防幻原理
✅ MVCC + ReadView是(对普通非锁定查询)多版本快照,不读新事务创建的版本
✅ 间隙锁机制是(对锁定读取)锁住查询范围,阻止插入/修改操作

3. MVCC 是如何防幻读的(“读时”控制)

REPEATABLE READ 下,InnoDB 对非加锁查询使用 MVCC(多版本)读取:

START TRANSACTION;
SELECT * FROM user WHERE age > 20;  -- 第一次查询,生成 ReadView
-- 别的事务插入一行 age = 25
SELECT * FROM user WHERE age > 20;  -- 第二次查询,不会看到新行 ✅
COMMIT;

✅ 原理:

  • 第一次查询时创建 ReadView

  • 后续所有查询都只读“老版本链”中对 ReadView 可见的版本

  • 别人插入的是新版本,不在快照中,不可见

  • 所以不会出现“幻影行”

⚠️ 局限:

MVCC 只是“读视图”避开幻读,并没有真正阻止幻读产生
👉 如果你是要做更新或删除操作,MVCC 是不够的!


4. Next-Key Lock 是如何防幻读的(“写时”控制)

✅ 什么是 Next-Key Lock?

InnoDB 为了防止幻读,在范围查询上加的锁叫做:

Next-Key Lock = 行锁(Record Lock)+ 间隙锁(Gap Lock)

它会把查询范围内的“已有行 + 可能插入的新位置”都锁住!

📌 示例

-- 原表中 age 有 10, 20, 30 三条记录-- 查询:
SELECT * FROM user WHERE age > 15 FOR UPDATE;-- 加锁范围:
(15,20]、(20,30]、(30, +∞)

👉 所有可能插入符合 age > 15 条件的点,都被锁住了。


✅ 幻读控制流程图

           范围:age > 15
┌─────┬────────┬────────┬────────────┐
│ 10  │  20    │   30   │   ∞        │
└─────┴────────┴────────┴────────────┘↑   ↑       ↑       ↑└─ (15,20]  (20,30]  (30,+∞)  ← 都被加锁其他事务无法在这些“间隙”中插入新记录 ✅

🧪 示例验证

-- 会话A
START TRANSACTION;
SELECT * FROM user WHERE age > 15 FOR UPDATE;-- 会话B(阻塞 ❌)
INSERT INTO user (id, name, age) VALUES (4, 'Bob', 25);  -- 被间隙锁阻止

🔒 会话 A 持有 (15, +∞) 的间隙锁,B 插入的 age = 25 正好落在间隙中,阻塞等待


5. InnoDB 底层如何实现间隙锁(Gap Lock)

InnoDB 使用 B+ Tree 存储记录,查询过程中会:

  1. 扫描范围:定位满足 WHERE 条件的记录位置

  2. 对这些记录本身加行锁(Record Lock)

  3. 对这些记录之间的空隙,加间隙锁(Gap Lock)

🔍 B+ Tree 节点示意:

┌────────────┐
│  key = 10  │
├────────────┤
│  key = 20  │
├────────────┤
│  key = 30  │
└────────────┘

对于 SELECT ... WHERE key > 15 FOR UPDATE

  • 加锁 key=20、30(Record Lock)

  • 同时加锁 (15,20)、(20,30)、(30,+∞) 的间隙(Gap Lock)


6. 源码层简要机制

InnoDB 核心函数:

row_search_for_mysql()└─ row_sel_build_range_access_path()└─ sel_set_rec_lock() → 加锁逻辑

锁的结构体:

struct lock_t {ulint type_mode; // record/gap/next-keyrec_t* record;dict_index_t* index;
};

间隙锁和 next-key lock 会设置特定位标识,并与索引项绑定。


7. 实际对比测试(验证幻读与否)

❌ 幻读发生(未加锁)

-- A:
START TRANSACTION;
SELECT * FROM user WHERE age > 20;-- B:
INSERT INTO user (id, name, age) VALUES (10, 'New', 25);
COMMIT;-- A:
SELECT * FROM user WHERE age > 20;  -- 多了一条 ✅,发生幻读

✅ 幻读避免(加锁)

-- A:
START TRANSACTION;
SELECT * FROM user WHERE age > 20 FOR UPDATE;-- B:
INSERT INTO user (id, name, age) VALUES (10, 'New', 25); -- 阻塞 ❌

8. 总结对比:幻读解决机制

机制防止方式场景
MVCC + ReadView不读新插入版本普通非锁定查询(如 SELECT)
Next-Key Lock直接阻止插入范围查询+更新/删除等操作
间隙锁锁定插入位置范围内插入被阻塞
行锁仅锁定已有记录无法防止幻读

9. 最佳实践

应用场景推荐写法
严格防止幻读(如金融场景)使用 SELECT ... FOR UPDATE
普通查询,关注一致性使用 REPEATABLE READ 即可
插入并发冲突配合唯一约束 + 锁
范围更新/删除明确加锁防幻插入

五、典型的幻读场景分析

1. 幻读测试准备

-- 创建测试表
CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(20),age INT
) ENGINE=InnoDB;-- 插入初始数据
INSERT INTO user VALUES (1, 'Alice', 20);
INSERT INTO user VALUES (2, 'Bob', 25);

设置隔离级别为默认的 REPEATABLE READ(InnoDB 默认使用 MVCC + Next-Key Lock)

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

2. 测试1:普通 SELECT 查询是否会发生幻读?(MVCC 生效)

🎯 目标:

  • 测试非加锁读是否看到新插入的符合条件的记录?

  • 用于演示 MVCC 防止幻读


🧵 会话A:

START TRANSACTION;SELECT * FROM user WHERE age > 22;
-- 返回:id=2, Bob, 25(仅 1 条)-- 此时生成 ReadView 快照

🧵 会话B:

START TRANSACTION;INSERT INTO user VALUES (3, 'Carl', 30);  -- 满足 age > 22
COMMIT;

🧵 会话A(再次查询):

SELECT * FROM user WHERE age > 22;
-- 结果仍然是:id=2, Bob, 25 ✅
-- 并未出现 Carl 的记录

✅ 结果分析:

  • 幻读 没有发生

  • 虽然会话B插入了一条符合条件的记录,但事务A使用的是快照 ReadView

  • 新插入的版本不在当前事务快照内,不可见


3. 测试2:使用 FOR UPDATE 显式加锁是否能阻止插入?(防幻插入)

🎯 目标:

  • 测试 SELECT ... FOR UPDATE 是否会锁住插入间隙

  • 演示 间隙锁 + Next-Key Lock 阻止插入


🧵 会话A:

START TRANSACTION;SELECT * FROM user WHERE age > 22 FOR UPDATE;
-- 会锁住已有记录(age=25),以及它之后的间隙(25,+∞)

🧵 会话B:

START TRANSACTION;INSERT INTO user VALUES (4, 'David', 28);  -- 落入间隙范围-- ⚠️ 阻塞!等待锁释放

🧵 会话A:

COMMIT;

🧵 会话B:

-- 插入现在才完成

✅ 结果分析:

  • 会话A加了 Next-Key Lock(间隙锁 + 行锁)

  • 插入操作被“锁定范围”阻塞

  • 成功防止了幻读插入(不仅不可读,还不可写)


4. 测试3:用 READ COMMITTED + 非加锁 SELECT 会发生幻读

🎯 目标:

  • 测试低隔离级别 READ COMMITTED 会不会看到新插入的行

  • 验证在非加锁读下幻读是可能的


🧵 会话A:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;SELECT * FROM user WHERE age > 22;
-- 返回 id=2 (Bob, 25)

🧵 会话B:

START TRANSACTION;
INSERT INTO user VALUES (5, 'Eva', 26);  -- age > 22
COMMIT;

🧵 会话A:

SELECT * FROM user WHERE age > 22;
-- ❗ 返回两条记录:Bob + Eva
-- 发生幻读了

✅ 结果分析:

  • 因为 READ COMMITTED 隔离级别每次查询都重新构建 ReadView

  • 所以新插入的记录对后续查询可见

  • 幻读确实发生了


5. 总结对比

测试编号场景是否幻读原因分析
1REPEATABLE READ + 非锁定查询❌ 没有MVCC + 快照一致性,插入对快照不可见
2FOR UPDATE 锁定查询范围❌ 没有Next-Key Lock 锁住插入间隙,插入阻塞
3READ COMMITTED + 普通查询✅ 有每次新 ReadView,插入的新数据对后续查询可见

6. 实战建议

需求场景推荐手段
保证两次查询结果一致使用 REPEATABLE READ 事务
严格禁止“条件插入”导致幻读使用 SELECT ... FOR UPDATE
查询实时数据使用 READ COMMITTED
高并发插入更新场景控制锁粒度、加唯一索引防止锁冲突

六、剖析INFORMATION_SCHEMA 的 3 张核心锁监控表

要深入了解 InnoDB 锁的状态与机制,就必须掌握如何实时查看、分析事务加锁情况、阻塞状态、锁等待链等信息。

MySQL 提供了 INFORMATION_SCHEMA 的 3 张核心锁监控表 —— INNODB_LOCKS, INNODB_LOCK_WAITS, INNODB_TRX,可以结合它们分析锁争用与事务阻塞。


1. 如何启用锁监控视图

从 MySQL 5.5 起,InnoDB 引入了锁监控表。

查看当前事务锁信息:

SELECT * FROM information_schema.innodb_locks;
SELECT * FROM information_schema.innodb_lock_waits;
SELECT * FROM information_schema.innodb_trx;

⚠️ 注意:

  • 这些表只会有活动事务产生数据(即:有加锁且未提交的事务)

  • 若你看到为空,说明当前无持锁事务


2. 3 张视图的作用详解

视图名称作用
INNODB_TRX展示当前所有活跃事务(事务ID、状态、运行时间、锁等待等)
INNODB_LOCKS展示所有持有或等待的锁(锁类型、锁对象、模式)
INNODB_LOCK_WAITS展示锁等待关系(谁等待谁)

3. 实战演练:两个事务竞争导致阻塞

我们创建以下环境用于演示:

CREATE TABLE t_lock_demo (id INT PRIMARY KEY,val VARCHAR(20)
) ENGINE=InnoDB;INSERT INTO t_lock_demo VALUES (1, 'a'), (2, 'b');

🧵 会话A:

START TRANSACTION;
SELECT * FROM t_lock_demo WHERE id = 1 FOR UPDATE;
-- A事务锁住了 id = 1 这一行

🧵 会话B:

START TRANSACTION;
UPDATE t_lock_demo SET val = 'z' WHERE id = 1;
-- ⚠️ 阻塞!因为等待 A 释放锁

🔍 这时我们执行:

SELECT * FROM information_schema.innodb_trx\G
SELECT * FROM information_schema.innodb_locks\G
SELECT * FROM information_schema.innodb_lock_waits\G

4. 字段详解与分析技巧

INNODB_TRX 核心字段

字段名含义
trx_id当前事务 ID(MySQL 内部 ID)
trx_state事务状态,如 RUNNING, LOCK WAIT
trx_started事务开始时间
trx_mysql_thread_idMySQL 线程 ID(可用来 KILL
trx_query当前事务执行的 SQL 语句
trx_wait_started若等待中,等待开始时间
trx_rows_locked已加锁行数

🧠 Tips:

  • 有事务为 LOCK WAIT 状态,说明它正在等待其他事务释放锁。


INNODB_LOCKS 核心字段

字段名含义
lock_id锁的唯一标识 ID
lock_mode锁模式(如 X, S, IX, IS 等)
lock_typeRECORDTABLE(行锁/表锁)
lock_table锁在哪张表
lock_index锁在哪个索引上(通常为主键或二级索引)
lock_data被锁住的主键值(记录锁)或索引范围(间隙锁)

🧠 Tips:

  • 多个事务锁定相同 lock_data,说明竞争发生在这行。


INNODB_LOCK_WAITS 核心字段

字段名含义
requesting_trx_id正在等待锁的事务 ID
blocking_trx_id持有锁的事务 ID
requested_lock_id等待中的锁 ID
blocking_lock_id阻塞它的锁 ID

🧠 Tips:

  • 这是分析死锁/阻塞链的关键视图!


5. 组合查询分析事务阻塞链(推荐 SQL)

1️⃣ 查询所有活跃事务:

SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx;

2️⃣ 查询锁等待关系(谁阻塞谁):

SELECT r.trx_id AS waiting_trx,r.trx_mysql_thread_id AS waiting_thread,r.trx_query AS waiting_query,b.trx_id AS blocking_trx,b.trx_mysql_thread_id AS blocking_thread,b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;

6. 实战场景分析(输出样例)

-- innodb_trx:trx_id: 1234
trx_state: LOCK WAIT
trx_query: UPDATE t_lock_demo SET val='z' WHERE id = 1;trx_id: 1233
trx_state: RUNNING
trx_query: SELECT * FROM t_lock_demo WHERE id=1 FOR UPDATE;-- innodb_lock_waits:requesting_trx_id: 1234
blocking_trx_id: 1233-- innodb_locks:lock_id: 1233:45:3
lock_mode: X
lock_type: RECORD
lock_table: test/t_lock_demo
lock_data: 1

🧠 分析:

  • 事务1233 已锁住了 id = 1,持有 X

  • 事务1234 想更新同一行,被阻塞

  • 阻塞链清晰、可追踪


7. 运维实战建议

目标工具/建议
观察事务是否阻塞查看 innodb_trx
分析锁等待链联表 innodb_trx + lock_waits
分析哪个键被锁住innodb_locks.lock_data
手动终止阻塞事务KILL <thread_id>
定期排查锁争用使用 performance_schema.events_waits_current

8. 总结

工具用途
INNODB_TRX显示当前事务状态
INNODB_LOCKS显示每个事务持有/等待的锁
INNODB_LOCK_WAITS显示锁等待依赖关系(谁等谁)
联表分析(推荐)快速定位死锁/阻塞事务链
performance_schemaMySQL 8.0 更强锁监控
http://www.lryc.cn/news/580214.html

相关文章:

  • Kuberrnetes 服务发布
  • AI领域新趋势:从提示(Prompt)工程到上下文(Context)工程
  • Spring Boot + 本地部署大模型实现:优化与性能提升
  • 【排序算法】
  • 模型部署与推理--利用libtorch模型部署与推理
  • 前端捕获异常的全面场景及方法
  • MYSQL 服务正在启动或停止中,请稍候片刻后再试一次。
  • Java 与 MySQL 性能优化:MySQL全文检索查询优化实践
  • 在 Ubuntu 22.04 上使用 Minikube 部署 Go 应用到 Kubernetes
  • 微服务架构下的抉择:Consul vs. Eureka,服务发现该如何选型?
  • 本地部署Dify并结合ollama大语言模型工具搭建自己的AI知识库
  • 软件反调试(4)- 基于IsDebuggerPresent的检测
  • Docker学习笔记:Docker网络
  • LDO VS DCDC
  • Redis的缓存击穿和缓存雪崩
  • [C++] C++多重继承:深入解析复杂继承关系
  • 每周资讯 | Krafton斥资750亿日元收购日本动画公司ADK;《崩坏:星穹铁道》新版本首日登顶iOS畅销榜
  • 小架构step系列04:springboot提供的依赖
  • XION:玩转您的第一个智能合约
  • WPS中配置MathType教程
  • Linux入门篇学习——Linux 帮助手册
  • 三、jenkins使用tomcat部署项目
  • 【开源品鉴】FRP源码阅读
  • LangChain 全面入门
  • 数据结构入门:链表
  • 服务器的IO性能怎么看?
  • 数据库11:MySQL 库的操作、库的说明与表的操作、表的说明
  • 电机转速控制系统算法分析与设计
  • 微信小程序如何实现再多个页面共享数据
  • 达梦数据库DMHS介绍及安装部署