mysql 从零单排之MVCC

MySQL MVCC(多版本并发控制)的工作流程及原理。

一、MVCC 是什么?

MVCC(Multi-Version Concurrency Control) 是一种并发控制机制,核心思想是:

读写不阻塞:写操作生成新版本,读操作读取旧版本

scss 复制代码
传统锁机制(读写互斥)          MVCC(读写并行)
┌─────────┐    ┌─────────┐      ┌─────────┐    ┌─────────┐
│  读操作  │◄──►│  写操作  │      │  读操作  │    │  写操作  │
│  (阻塞)  │    │  (阻塞)  │      │ (读旧版) │    │ (写新版) │
└─────────┘    └─────────┘      └─────────┘    └─────────┘
        互相等待                        互不干扰

二、核心概念:三个隐藏字段

InnoDB 每行记录都有 3 个隐藏字段

字段名 长度 作用
DB_TRX_ID 6 字节 最后修改该行的事务 ID
DB_ROLL_PTR 7 字节 回滚指针,指向 undo log
DB_ROW_ID 6 字节 隐藏主键(若无显式主键)
bash 复制代码
┌─────────────────────────────────────────────────────────┐
│                    行记录结构示例                         │
├─────────────────────────────────────────────────────────┤
│  id  │  name  │  age  │  DB_TRX_ID  │  DB_ROLL_PTR     │
│  1   │  张三   │  20   │     100     │  0x7f8b...      │
└─────────────────────────────────────────────────────────┘
         ↑ 用户可见字段              ↑ 隐藏字段(对用户透明)

三、核心概念:Undo Log(回滚日志)

Undo Log 是 MVCC 的版本链基础,记录数据修改前的状态:

arduino 复制代码
版本链(同一行数据的多个版本):

┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│  版本3   │◄────│  版本2   │◄────│  版本1   │◄────│  初始版本 │
│ TRX_ID= │     │ TRX_ID= │     │ TRX_ID= │     │ TRX_ID= │
│   300   │     │   200   │     │   100   │     │    50   │
│  name=  │     │  name=  │     │  name=  │     │  name=  │
│ "王五"  │     │ "李四"  │     │ "张三"  │     │ "初始"  │
└────┬────┘     └─────────┘     └─────────┘     └─────────┘
     │
     └── DB_ROLL_PTR 指向这里(最新版本指向旧版本)

四、核心概念:Read View(读视图)

Read View 是快照读时的"可见性判断器",决定当前事务能看到哪个版本的数据。

Read View 的四个关键属性:

sql 复制代码
┌─────────────────────────────────────────────────────────┐
│                    Read View 结构                        │
├─────────────────────────────────────────────────────────┤
│  creator_trx_id    │  创建该 Read View 的事务 ID          │
│  m_ids             │  活跃事务 ID 列表(未提交的事务)      │
│  min_trx_id        │  最小活跃事务 ID                     │
│  max_trx_id        │  下一个分配的事务 ID(已创建的最大+1)  │
└─────────────────────────────────────────────────────────┘

可见性判断规则(核心算法):

python 复制代码
def is_visible(trx_id, read_view):
    """
    判断某版本的 trx_id 对当前事务是否可见
    """
    if trx_id == read_view.creator_trx_id:
        # 规则1:自己修改的数据,总是可见
        return True
    
    if trx_id < read_view.min_trx_id:
        # 规则2:在 Read View 创建前已提交,可见
        return True
    
    if trx_id >= read_view.max_trx_id:
        # 规则3:在 Read View 创建后才启动,不可见
        return False
    
    if trx_id in read_view.m_ids:
        # 规则4:在活跃列表中(未提交),不可见
        return False
    
    # 规则5:不在活跃列表中(已提交),可见
    return True

五、MVCC 工作流程图解

场景:事务 A 读取,事务 B 修改同一行

ini 复制代码
时间线 ───────────────────────────────────────────────►

T1: 事务 A 启动(TRX_ID=100)
    └─► 创建 Read View: {creator=100, m_ids=[100], min=100, max=101}
    
T2: 事务 B 启动(TRX_ID=101)
    └─► 开始修改 id=1 的行
    
T3: 事务 B 修改 id=1 的 name="李四"(未提交)
    └─► 生成新版本:TRX_ID=101, 旧版本进入 Undo Log
        ┌─────────┐
        │ 新版本   │ TRX_ID=101, name="李四"
        └────┬────┘
             │ DB_ROLL_PTR
             ▼
        ┌─────────┐
        │ 旧版本   │ TRX_ID=50, name="张三"  ◄── 事务 A 能看到
        └─────────┘
        
T4: 事务 A 查询 id=1
    └─► 检查最新版本 TRX_ID=101
        ├─► 101 >= max_trx_id(101)? 是,但 101 在 m_ids 中
        ├─► 101 在 m_ids 中?是,不可见!
        └─► 沿回滚指针找到旧版本 TRX_ID=50
            ├─► 50 < min_trx_id(100)?是,可见!
            └─► 返回 name="张三" ✓
            
T5: 事务 B 提交
    
T6: 事务 A 再次查询 id=1(同一事务内)
    └─► 仍使用 T1 创建的 Read View
    └─► 结果仍是 name="张三"(可重复读!)

六、RC vs RR 的区别:Read View 创建时机

隔离级别 Read View 创建时机 效果
RC(读已提交) 每条 SELECT 都创建新的 能看到其他事务已提交的最新数据
RR(可重复读) 事务启动时创建,全程复用 事务内多次读取结果一致
sql 复制代码
RC 工作流程:                        RR 工作流程:

SELECT #1 ──► 创建 RV1              START ──────► 创建 RV
SELECT #2 ──► 创建 RV2 (看到新提交)   SELECT #1 ──► 使用 RV
SELECT #3 ──► 创建 RV3 (看到新提交)   SELECT #2 ──► 使用 RV(同结果)
                                     SELECT #3 ──► 使用 RV(同结果)

七、完整示例演示

表结构:

sql 复制代码
CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(20),
    age INT
) ENGINE=InnoDB;

INSERT INTO user VALUES (1, '张三', 20);  -- 假设 TRX_ID=1 插入

并发事务执行:

时间 事务 A (TRX_ID=100) 事务 B (TRX_ID=101) 数据版本链
t1 START TRANSACTION; V1: TRX_ID=1, name="张三"
t2 START TRANSACTION;
t3 UPDATE user SET name='李四' WHERE id=1; V2: TRX_ID=101 → V1
t4 SELECT * FROM user WHERE id=1; 返回 "张三"
t5 COMMIT; V2 已提交
t6 SELECT * FROM user WHERE id=1; RR返回"张三",RC返回"李四"
t7 COMMIT;

t4 时刻的可见性判断(RR):

ini 复制代码
Read View: {creator=100, m_ids=[100,101], min=100, max=102}

检查 V2 (TRX_ID=101):
  - 101 != 100(不是自己)
  - 101 >= 102? 否
  - 101 在 m_ids 中? 是 → 不可见!

沿回滚指针找到 V1 (TRX_ID=1):
  - 1 < 100? 是 → 可见!
  
结果: name="张三"

八、MVCC 解决并发问题

问题 解决方案
脏读(Dirty Read) 读取时判断,未提交事务的版本不可见
不可重复读(Non-repeatable Read) RR 级别复用 Read View
幻读(Phantom Read) 部分解决(快照读),当前读需加间隙锁

注意:MVCC 不解决幻读的场景

sql 复制代码
-- 事务 A
START TRANSACTION;
SELECT * FROM user WHERE age > 18;  -- 快照读,返回 2 条(MVCC 保证)

-- 事务 B 插入新记录并提交

SELECT * FROM user WHERE age > 18;  -- 仍是 2 条(MVCC 保证)
SELECT * FROM user WHERE age > 18 FOR UPDATE;  -- 当前读!返回 3 条(幻读!)

九、总结图

css 复制代码
┌─────────────────────────────────────────────────────────┐
│                      MVCC 核心架构                       │
├─────────────────────────────────────────────────────────┤
│  存储层: 行记录 + 隐藏字段(DB_TRX_ID, DB_ROLL_PTR)        │
│     │                                                   │
│     ▼                                                   │
│  版本链: Undo Log 串联的历史版本                         │
│     │                                                   │
│     ▼                                                   │
│  可见性: Read View 判断哪个版本对当前事务可见              │
│     │                                                   │
│     ▼                                                   │
│  隔离性: RC(每次读新建) / RR(事务开始时建)                 │
└─────────────────────────────────────────────────────────┘

总结 :MVCC 通过保存数据的历史版本 + 事务可见性判断 ,实现了读不阻塞写,写不阻塞读的高效并发控制。

相关推荐
GreatSQL2 小时前
参数配置不当导致GreatSQL异步复制IO线程中断
后端
老马95272 小时前
opencode3-我的能力超乎你的想象
人工智能·后端
weixin_408099672 小时前
【企业级方案】财务自动化:OCR发票识别 + 自动录入系统完整实现(附代码与落地架构)
后端·ocr·api·发票 ocr 识别·发票自动录入系统·发票识别 api·财务自动化
千寻简2 小时前
一个让 Claude Code 顺手很多的状态栏插件:claude-hud
前端·后端
掘金者阿豪2 小时前
数据库安全第一关:用户密码存储与认证机制的深度拆解
java·前端·后端
MgArcher2 小时前
Python高级特性:sorted() 排序完全指南
前端·后端
MgArcher2 小时前
Python高级特性:返回函数与闭包完全指南
前端·后端
未秃头的程序猿2 小时前
💥 MyBatis 面试连环炮:从源码原理到实战避坑,彻底拿下 Offer 通关秘籍
后端·面试·mybatis
Java编程爱好者2 小时前
深入浅出 Java volatile:从硬件到 JMM 的完整剖析
后端