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 = 200DB_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 | 生成新 ReadView200 < 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=200200 < 300,说明在 ReadView2 生成前已提交,可见 ,返回 balance=200

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

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


下载案例图解:

相关推荐
名字还没想好☜10 小时前
Python f-string 进阶:数字格式化、对齐填充、调试 = 号与嵌套表达式
开发语言·数据库·python·字符串格式化·f-string
ltl10 小时前
Serverless 数据库弹性理论:Neon 与 Aurora Serverless v2
数据库
·薯条大王10 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
Dxy123931021611 小时前
Python 如何使用 MySQL 的事务
python·mysql
ZJH__GO13 小时前
网络编程v4pro--实现聊天室文件传输功能
java·服务器·网络·计算机网络
程序员黑豆13 小时前
Windows 系统 Java 环境变量配置全攻略:解决“不是内部或外部命令”
java·前端·ai编程
命运之光14 小时前
【C语言完整代码】就诊信息管理系统
java·c语言·开发语言
凤山老林14 小时前
Spring Boot @Async 线上实战:从默认配置到生产级线程池治理
java·spring boot·后端
marvelyu14 小时前
每天10分钟学会OceanBase系列(Day 20):跨机房容灾实战——构建多数据中心高可用架构
java·大数据·数据库
ZCBUS实时计算15 小时前
金融证券实时数仓建设实践:轻量化实时计算平台落地,实现交易数据端到端秒级处理
大数据·数据库·数据仓库·金融·flink·dba·etl