深入剖析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=100

UPDATE age = 40  => Undo2: age = 30, trx_id=101
                   Undo1: 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 的限制

限制项 描述
只适用于 SELECT DELETE, 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-key
    rec_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. 总结对比

测试编号 场景 是否幻读 原因分析
1 REPEATABLE READ + 非锁定查询 ❌ 没有 MVCC + 快照一致性,插入对快照不可见
2 FOR UPDATE 锁定查询范围 ❌ 没有 Next-Key Lock 锁住插入间隙,插入阻塞
3 READ 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_id MySQL 线程 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_type RECORDTABLE(行锁/表锁)
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
定期排查锁争用 使用 performance_schema.events_waits_current

8. 总结

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

你可能感兴趣的:(MySQL,mysql,数据库)