MVCC 可见性判断规则 —案例深度讲解


一、案例背景

假设有一张 account 表:

sql 复制代码
CREATE TABLE account (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance INT
);

INSERT INTO account VALUES (1, 'Alice', 100);

InnoDB 底层,这行数据除了用户可见的字段外,还隐藏着两个关键系统字段:

|---------------|--------|---------------------------------|
| 隐藏字段 | 值(初始) | 含义 |
| DB_TRX_ID | 50 | 最后修改这行数据的事务 ID(假设是初始化事务 50) |
| DB_ROLL_PTR | NULL | 回滚指针,指向 Undo Log 中的旧版本 |


二、时间线与版本链演变

下面按照图中的 T1 ~ T6 时间线,还原完整过程。

T1 时刻:事务 100 开始

复制代码
-- 事务100
START TRANSACTION;          -- 事务ID = 100
SELECT * FROM account WHERE id = 1;

此时生成 ReadView1:

复制代码
ReadView1:
├─ m_ids       = {100}      ← 当前活跃事务只有100自己
├─ min_trx_id  = 100        ← m_ids中最小值
├─ max_trx_id  = 101        ← 系统下一个待分配的事务ID
└─ creator_trx_id = 100     ← 创建者就是事务100自己

可见性判断:

  • 读取当前行:trx_id = 50
  • 50 < min_trx_id(100) → 在 ReadView1 生成前已提交 → 可见
  • 返回 balance = 100

T2 时刻:事务 200 开始并修改数据

复制代码
-- 事务200
START TRANSACTION;          -- 事务ID = 200
UPDATE account SET balance = 200 WHERE id = 1;
-- 暂不提交

InnoDB 内部发生了什么?

  1. 将旧数据 (trx_id=50, balance=100) 拷贝到 Undo Log
  2. 修改当前行:balance = 200,DB_TRX_ID = 200
  3. 当前行的 DB_ROLL_PTR 指向 Undo Log 中的旧版本

此时版本链:

复制代码
当前行(数据页)          Undo Log
├─ trx_id = 200    ←───┐
├─ balance = 200       │
├─ roll_ptr ───────────┘
                       └─ trx_id = 50
                          balance = 100
                          roll_ptr = NULL

注意 :事务 200 未提交,但修改已经写入数据页。其他事务能否看到这个 200?取决于 ReadView 的判断。


T3 时刻:事务 100 再次 SELECT(RR 级别,复用 ReadView1)

复制代码
-- 事务100(仍在事务中)
SELECT * FROM account WHERE id = 1;   -- 第二次查询

RR 级别规则 :复用 T1 时刻生成的 ReadView1。

Step 1:读取当前行 → trx_id = 200

判断流程(对照图中 [T3] 部分):

|-------------------------------------|--------------------|------------|
| 判断条件 | 计算 | 结果 |
| trx_id == creator_trx_id | 200 == 100 | ❌ 否 |
| trx_id < min_trx_id | 200 < 100 | ❌ 否 |
| trx_id >= max_trx_id | 200 >= 101 | ❌ 否 |
| min_trx_id <= trx_id < max_trx_id | 100 <= 200 < 201 | ⚠️ 是,进入下一步 |
| trx_id 是否在 m_ids 中 | 200 ∈ {100} | ❌ 不在 |

关键分析:

  • 200 不在 m_ids 中,说明 T1 时刻事务 200 还未开始
  • 但当前行 trx_id=200 已经存在,说明这是 T1 之后创建的新版本
  • 由于 ReadView1 是在 T1 生成的,事务 100 只能看到 T1 之前已提交的数据
  • 结论: ❌ 不可见!

Step 2:沿 DB_ROLL_PTR找到 Undo Log 1 → trx_id = 100

|----------------------------|--------------|----------|
| 判断条件 | 计算 | 结果 |
| trx_id == creator_trx_id | 100 == 100 | ✅ 是! |

结论: ✅ 可见!返回 balance = 100

事务 100 第二次查询,仍然看到 100,虽然事务 200 已经把数据改成了 200。 这就是 MVCC 实现的可重复读!


T4 时刻:事务 200 提交

复制代码
-- 事务200
COMMIT;

事务 200 提交后,它的修改成为数据库的"最新值"。但 Undo Log 不会立即删除(因为可能还有其他事务需要读取历史版本)。


T5 时刻:事务 300 开始并 SELECT

复制代码
-- 事务300
START TRANSACTION;          -- 事务ID = 300
SELECT * FROM account WHERE id = 1;

此时生成 ReadView2:

复制代码
ReadView2:
├─ m_ids       = {300}
├─ min_trx_id  = 300
├─ max_trx_id  = 301
└─ creator_trx_id = 300

Step 1:读取当前行 → trx_id = 200

|----------------------------|--------------|----------|
| 判断条件 | 计算 | 结果 |
| trx_id == creator_trx_id | 200 == 300 | ❌ 否 |
| trx_id < min_trx_id | 200 < 300 | ✅ 是! |

结论: ✅ 可见!返回 balance = 200

200 < 300意味着:事务 200 在 ReadView2 生成之前就已经提交了。 所以事务 300 能看到最新值。


T6 时刻:事务 100 第三次 SELECT(仍在事务中)

复制代码
-- 事务100(还没提交!)
SELECT * FROM account WHERE id = 1;   -- 第三次查询

RR 级别 :仍然复用 ReadView1(T1 生成的那个)。

判断过程与 T3 完全一致:

|----------------------------|---------------|-----|
| 判断条件 | 计算 | 结果 |
| trx_id == creator_trx_id | 200 == 100 | ❌ 否 |
| trx_id < min_trx_id | 200 < 100 | ❌ 否 |
| trx_id >= max_trx_id | 200 >= 101 | ❌ 否 |
| trx_id 在 m_ids{100} 中 | 200 ∈ {100} | ❌ 否 |

结论: ❌ 不可见!沿版本链继续找 → Undo Log 1 ( trx_id=100) → ✅ 可见!

返回 balance = 100。

即使事务 200 已经提交,事务 100 仍然看到 100。 这就是 RR(可重复读) 的精髓 ------ 事务内看到的数据快照始终一致。


三、可见性判断规则总结(对照图)

复制代码
┌────────────────────────────────────────────────────────────────────────┐
│                        可见性判断算法(核心)                           │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  ① trx_id == creator_trx_id ?                                         │
│     → YES: 自己修改的,可见 ✅                                          │
│                                                                        │
│  ② trx_id < min_trx_id ?                                              │
│     → YES: 在 ReadView 生成前已提交,可见 ✅                            │
│                                                                        │
│  ③ trx_id >= max_trx_id ?                                             │
│     → YES: 在 ReadView 生成后才创建,不可见 ❌(找旧版本)               │
│                                                                        │
│  ④ min_trx_id <= trx_id < max_trx_id ?                                │
│     → 检查 trx_id 是否在 m_ids 中:                                     │
│        • IN m_ids  → 生成 ReadView 时仍活跃(未提交),不可见 ❌         │
│        • NOT IN m_ids → 生成 ReadView 前已提交,可见 ✅                  │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

四、RC vs RR 的关键区别(案例对比)

|---------------------|------------------------------------------------|----------------------------|
| 场景 | RC(读已提交) | RR(可重复读) |
| T3 事务100 SELECT | 生成新 ReadView,看到 200(脏读?不,200未提交)→ 实际看到 100 | 复用 ReadView1,看到 100 |
| T5 事务300 SELECT | 生成新 ReadView,看到 200 | 生成新 ReadView,看到 200 |
| T6 事务100 SELECT | 生成新 ReadView ,200 < min_trx_id,看到 200 | 复用 ReadView1 ,看到 100 |

核心差异:

复制代码
RC: 每次 SELECT 都生成新 ReadView
    → 能读到其他事务最新已提交的数据
    → 同一事务内两次读取结果可能不同(不可重复读)

RR: 事务内第一次 SELECT 生成 ReadView,之后复用
    → 整个事务期间数据快照一致
    → 同一事务内多次读取结果相同(可重复读)

五、总结

"MVCC 的可见性判断,我用一个实际案例来说明:

假设表里有 balance=100,事务 100 先开始并查询,生成 ReadView1(m_ids={100}, min=100, max=101)。此时事务 200 把 balance 改成 200 但未提交。事务 100 再次查询时,读取当前行 trx_id=200,发现 200 不在 m_ids 中且 200 >= max_trx_id 不成立,说明这是 ReadView1 生成之后才出现的版本,不可见 。于是沿着 DB_ROLL_PTR 回滚指针找到 Undo Log 中的旧版本 trx_id=100,发现就是自己修改的,可见 ,返回 balance=100。

之后事务 200 提交,事务 300 开始查询,生成 ReadView2(min=300),读取当前行 trx_id=200,200 < 300,说明在 ReadView2 生成前已提交,可见 ,返回 balance=200。

而事务 100 第三次查询,RR 级别下仍然复用 ReadView1,判断逻辑和第二次完全一致,仍然返回 balance=100。这就实现了可重复读。

判断规则的本质就是四步比较:creator_trx_id → min_trx_id → max_trx_id → m_ids,确保事务只能看到在它 ReadView 生成之前已经提交的数据。"


下载案例图解:

相关推荐
wno704几秒前
Spring Boot整合Flyway
java·spring boot·后端
vx_Biye_Design5 分钟前
springboot旅游管理系统18006-计算机课程设计、毕业设计
java·spring boot·后端·python·elasticsearch·django·课程设计
Wang's Blog9 分钟前
Java 中间件之 RabbitMQ 快速入门: 简单队列模型快速入门
java·中间件·java-rabbitmq
anxiao_m15 分钟前
跨地域大文件怎么传?2026主流传输软件实测对比
大数据·数据库·文件传输
Su米苏17 分钟前
基于 Token 预算的上下文压缩控制器(Context Compaction Controller)
前端·数据库·人工智能
骇客野人17 分钟前
Java 开发组件大全覆盖后端主流技术栈(基础、Web、ORM、中间件、微服务、安全、工具、测试、运维、信创适配常用组件)
java·前端·中间件
暖核21 分钟前
Redis 从基础到集群实战:数据类型、客户端、高可用架构完整梳理
数据库·redis·架构
2401_8888597139 分钟前
STM32H733 MPU、AXI、FMC学习
java·开发语言·stm32·spring
神一样的老师41 分钟前
WS63 访问 HTTPS 握手失败(-0x7780)根治
数据库·网络协议·https
Patrick在香港1 小时前
时间戳凭空早了 8 小时:datetime.utcnow() 弃用实测与漂移复盘
数据库·python·标准库·datetime·时区·弃用