MySQL MVCC(多版本并发控制)详解

目录

  1. MVCC概述
  2. MVCC的核心组件
  3. MVCC工作原理
  4. 事务隔离级别与MVCC
  5. MVCC的优势与局限性
  6. 实际应用示例
  7. 总结

MVCC概述

MVCC(Multi-Version Concurrency Control,多版本并发控制) 是MySQL InnoDB存储引擎实现高并发的核心技术。它通过保存数据在某个时间点的快照来实现并发控制,使得读操作不会阻塞写操作,写操作也不会阻塞读操作,从而大大提高了数据库的并发性能。

MVCC的基本思想

MVCC的核心思想是:

  • 为每个事务提供一个数据库的快照
  • 读操作读取快照数据,不需要加锁
  • 写操作创建新版本,不直接覆盖旧版本
  • 通过版本链和可见性判断来实现数据一致性

MVCC整体架构

graph TB
    subgraph "事务层"
        T1[事务1]
        T2[事务2]
        T3[事务3]
    end

    subgraph "MVCC核心组件"
        RV[Read View]
        UL[Undo Log]
        HF[隐藏字段]
    end

    subgraph "存储层"
        CR[当前记录]
        VC[版本链]
        IX[索引]
    end

    T1 --> RV
    T2 --> RV
    T3 --> RV

    RV --> HF
    HF --> CR
    CR --> VC
    VC --> UL

    style RV fill:#e1f5fe
    style UL fill:#f3e5f5
    style HF fill:#e8f5e8

MVCC的核心组件

三个隐藏字段

InnoDB为每一行记录都添加了三个隐藏字段,这些字段是MVCC实现的基础:

1. DB_TRX_ID(事务ID)

  • 长度:6字节
  • 作用:记录最后一次修改该行记录的事务ID
  • 特点:事务ID是递增的,新事务的ID总是大于旧事务的ID

2. DB_ROLL_PTR(回滚指针)

  • 长度:7字节
  • 作用:指向该行记录的上一个版本,存储在Undo Log中
  • 特点:通过回滚指针可以找到该记录的历史版本,形成版本链

3. DB_ROW_ID(行ID)

  • 长度:6字节
  • 作用:当表没有主键和唯一键时,InnoDB会自动生成该字段作为聚簇索引
  • 特点:单调递增的行标识符
-- 表结构示例
CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT
    -- 隐藏字段(不可见):
    -- DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID
);

Undo Log(撤销日志)

Undo Log 是MVCC实现的关键组件,它保存了数据修改前的版本信息。

Undo Log的作用

  1. 事务回滚:当事务需要回滚时,可以利用Undo Log恢复数据
  2. MVCC读取:为MVCC提供历史版本数据
  3. 崩溃恢复:数据库崩溃后的数据恢复

Undo Log的类型

  • Insert Undo Log:记录INSERT操作,只在事务回滚时需要
  • Update Undo Log:记录UPDATE和DELETE操作,既用于回滚也用于MVCC

版本链的形成

graph LR
    A[当前记录] --> B[Undo Log 1]
    B --> C[Undo Log 2]
    C --> D[Undo Log 3]
    D --> E[...]
    
    A -.->|DB_ROLL_PTR| B
    B -.->|DB_ROLL_PTR| C
    C -.->|DB_ROLL_PTR| D

Read View(读视图)

Read View 是事务在执行查询操作时创建的一个数据结构,用于判断版本链中哪些版本对当前事务可见。

Read View的核心字段

class ReadView {
private:
    trx_id_t m_low_limit_id;      // 当前系统中最大的事务ID + 1
    trx_id_t m_up_limit_id;       // 当前系统中最小的活跃事务ID
    trx_id_t m_creator_trx_id;    // 创建该ReadView的事务ID
    trx_id_t* m_ids;              // 创建ReadView时的活跃事务ID列表
    ulint m_size;                 // 活跃事务数量
};

可见性判断算法

Read View通过以下算法判断记录版本是否可见:

bool is_visible(trx_id_t trx_id) {
    // 1. 如果记录的事务ID等于当前事务ID,可见
    if (trx_id == m_creator_trx_id) {
        return true;
    }

    // 2. 如果记录的事务ID小于最小活跃事务ID,可见
    if (trx_id < m_up_limit_id) {
        return true;
    }

    // 3. 如果记录的事务ID大于等于最大事务ID,不可见
    if (trx_id >= m_low_limit_id) {
        return false;
    }

    // 4. 如果记录的事务ID在活跃事务列表中,不可见
    if (is_in_active_list(trx_id)) {
        return false;
    }

    // 5. 其他情况可见
    return true;
}

Read View可见性判断流程图

flowchart TD
    A[获取记录的trx_id] --> B{trx_id == creator_trx_id?}
    B -->|是| C[可见 - 自己的修改]
    B -->|否| D{trx_id < up_limit_id?}
    D -->|是| E[可见 - 已提交的历史事务]
    D -->|否| F{trx_id >= low_limit_id?}
    F -->|是| G[不可见 - 未来事务]
    F -->|否| H{trx_id在活跃列表中?}
    H -->|是| I[不可见 - 未提交事务]
    H -->|否| J[可见 - 已提交事务]

    style C fill:#c8e6c9
    style E fill:#c8e6c9
    style J fill:#c8e6c9
    style G fill:#ffcdd2
    style I fill:#ffcdd2

MVCC工作原理

MVCC读取流程

flowchart TD
    A[开始查询] --> B[创建/获取ReadView]
    B --> C[获取记录最新版本]
    C --> D{版本是否可见?}
    D -->|是| E[返回该版本数据]
    D -->|否| F[通过DB_ROLL_PTR找到上一版本]
    F --> G{是否还有历史版本?}
    G -->|是| D
    G -->|否| H[返回空结果]

具体工作步骤

  1. 事务开始时

    • 为事务分配唯一的事务ID
    • 根据隔离级别决定何时创建ReadView
  2. 执行查询时

    • 创建或使用已有的ReadView
    • 从聚簇索引获取记录的最新版本
    • 使用ReadView判断版本可见性
  3. 版本不可见时

    • 通过DB_ROLL_PTR找到Undo Log中的历史版本
    • 重复可见性判断,直到找到可见版本或遍历完版本链
  4. 返回结果

    • 返回第一个可见的版本数据
    • 如果没有可见版本,返回空结果

事务隔离级别与MVCC

READ COMMITTED(读已提交)

在RC隔离级别下:

  • 每次查询都会创建新的ReadView
  • 只能读取已提交事务的数据
  • 可能出现不可重复读现象
-- 事务A
BEGIN;
SELECT * FROM user WHERE id = 1; -- 创建ReadView1,读取版本V1
-- 此时事务B提交了对id=1记录的修改
SELECT * FROM user WHERE id = 1; -- 创建ReadView2,读取版本V2
COMMIT;

REPEATABLE READ(可重复读)

在RR隔离级别下:

  • 第一次查询时创建ReadView,后续查询复用
  • 保证在同一事务中多次读取结果一致
  • MySQL默认隔离级别
-- 事务A
BEGIN;
SELECT * FROM user WHERE id = 1; -- 创建ReadView,读取版本V1
-- 此时事务B提交了对id=1记录的修改
SELECT * FROM user WHERE id = 1; -- 复用ReadView,仍读取版本V1
COMMIT;

隔离级别对比

隔离级别 ReadView创建时机 可重复读 幻读防护
READ COMMITTED 每次查询
REPEATABLE READ 第一次查询 部分✅

MVCC的优势与局限性

优势

  1. 高并发性能

    • 读操作不加锁,不阻塞写操作
    • 写操作不阻塞读操作
    • 大大提高了系统的并发处理能力
  2. 数据一致性

    • 通过版本控制保证事务的一致性视图
    • 避免了脏读、不可重复读等问题
  3. 无死锁风险

    • 读操作不需要获取锁
    • 减少了死锁的可能性

局限性

  1. 存储开销

    • 需要额外存储历史版本数据
    • Undo Log占用存储空间
  2. 版本链过长

    • 长时间运行的事务可能导致版本链过长
    • 影响查询性能
  3. 幻读问题

    • MVCC无法完全解决幻读问题
    • 需要结合Next-Key Lock解决
  4. 写操作冲突

    • 写-写操作仍需要加锁
    • 可能产生锁等待

MVCC与锁机制的协作

MVCC主要解决读-写冲突,但仍需要锁机制处理写-写冲突:

graph TD
    A[数据库操作] --> B{操作类型}
    B -->|读操作| C[使用MVCC]
    B -->|写操作| D[需要加锁]

    C --> E[创建/使用ReadView]
    E --> F[版本链遍历]
    F --> G[返回可见版本]

    D --> H{是否有锁冲突?}
    H -->|无冲突| I[获取锁并执行]
    H -->|有冲突| J[等待或死锁检测]

    I --> K[更新记录]
    K --> L[写入Undo Log]

    style C fill:#e8f5e8
    style D fill:#fff3e0
    style J fill:#ffebee

不同操作的处理方式

操作类型 处理机制 是否加锁 说明
SELECT MVCC 通过ReadView读取一致性快照
INSERT 锁 + MVCC 需要获取插入意向锁
UPDATE 锁 + MVCC 需要获取行锁,更新后写入Undo Log
DELETE 锁 + MVCC 需要获取行锁,标记删除并写入Undo Log

实际应用示例

示例场景

假设有一个用户表,我们来演示MVCC的工作过程:

CREATE TABLE user (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance DECIMAL(10,2)
);

INSERT INTO user VALUES (1, 'Alice', 1000.00);

并发事务示例

-- 初始状态:id=1, name='Alice', balance=1000.00, trx_id=100

-- 事务A (trx_id=101)
BEGIN;
UPDATE user SET balance = 900.00 WHERE id = 1;
-- 此时记录:id=1, name='Alice', balance=900.00, trx_id=101
-- Undo Log:id=1, name='Alice', balance=1000.00, trx_id=100

-- 事务B (trx_id=102) - 在事务A未提交时
BEGIN;
SELECT * FROM user WHERE id = 1;
-- ReadView: m_creator_trx_id=102, m_ids=[101,102], m_up_limit_id=101
-- 判断trx_id=101在活跃列表中,不可见
-- 通过Undo Log读取:balance=1000.00

-- 事务A提交
COMMIT;

-- 事务B再次查询(RR隔离级别)
SELECT * FROM user WHERE id = 1;
-- 仍使用原ReadView,读取balance=1000.00(可重复读)
COMMIT;

事务执行时序图

sequenceDiagram
    participant T1 as 事务A(101)
    participant T2 as 事务B(102)
    participant DB as 数据库
    participant UL as Undo Log

    Note over DB: 初始: balance=1000, trx_id=100

    T1->>DB: BEGIN
    T2->>DB: BEGIN

    T1->>DB: UPDATE balance=900
    DB->>UL: 保存旧版本(balance=1000, trx_id=100)
    Note over DB: 当前: balance=900, trx_id=101

    T2->>DB: SELECT * (创建ReadView)
    Note over T2: ReadView: [101,102]
    DB->>T2: 检查trx_id=101在活跃列表
    UL->>T2: 返回balance=1000 (历史版本)

    T1->>DB: COMMIT
    Note over DB: 事务101已提交

    T2->>DB: SELECT * (复用ReadView)
    Note over T2: 仍使用原ReadView: [101,102]
    UL->>T2: 返回balance=1000 (保证可重复读)

    T2->>DB: COMMIT

MVCC版本链示意图

graph TD
    A[当前记录<br/>id=1, balance=900, trx_id=101] 
    B[Undo Log 1<br/>id=1, balance=1000, trx_id=100]
    C[Undo Log 2<br/>id=1, balance=800, trx_id=99]
    
    A -->|DB_ROLL_PTR| B
    B -->|DB_ROLL_PTR| C
    
    D[事务B ReadView<br/>活跃事务: [101,102]] -.->|不可见| A
    D -.->|可见| B

性能优化建议

1. 控制长事务

长时间运行的事务会导致版本链过长,影响查询性能:

-- 避免长事务
BEGIN;
-- 大量操作...
-- 长时间不提交
COMMIT; -- 延迟提交导致版本链堆积

优化建议

  • 及时提交事务,避免长时间持有事务
  • 将大事务拆分为多个小事务
  • 监控事务执行时间

2. 合理设置隔离级别

根据业务需求选择合适的隔离级别:

-- 对于不需要可重复读的场景,可以使用RC
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 对于需要严格一致性的场景,使用RR(默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

3. 监控Undo Log大小

定期检查Undo Log的使用情况:

-- 查看Undo Log使用情况
SELECT
    tablespace_name,
    file_name,
    total_extents,
    extent_size,
    initial_size,
    autoextend_size
FROM information_schema.files
WHERE tablespace_name LIKE '%undo%';

-- 查看当前活跃事务
SELECT
    trx_id,
    trx_state,
    trx_started,
    trx_isolation_level,
    trx_rows_locked,
    trx_rows_modified
FROM information_schema.innodb_trx;

4. 配置参数优化

关键的MVCC相关参数:

# my.cnf配置示例
[mysqld]
# Undo Log相关
innodb_undo_tablespaces = 3          # Undo表空间数量
innodb_undo_log_truncate = ON        # 启用Undo Log截断
innodb_max_undo_log_size = 1G        # Undo Log最大大小

# 事务相关
innodb_purge_threads = 4             # 清理线程数量
innodb_purge_batch_size = 300        # 批量清理大小

总结

MySQL的MVCC机制是一个精妙的设计,它通过以下核心技术实现了高效的并发控制:

  1. 三个隐藏字段:为每行记录提供版本信息和链接能力
  2. Undo Log:保存历史版本,形成版本链
  3. Read View:提供事务级别的一致性视图
  4. 可见性算法:精确判断哪些版本对当前事务可见

MVCC使得MySQL在保证ACID特性的同时,实现了优秀的并发性能,是现代数据库系统的重要技术基础。理解MVCC的工作原理,对于数据库性能优化和问题排查具有重要意义。

关键要点

  • MVCC主要解决读-写冲突,提高并发性能
  • 不同隔离级别下ReadView的创建时机不同
  • 版本链的长度影响查询性能,需要及时清理Undo Log
  • MVCC与锁机制结合,共同保证数据库的ACID特性

本文档详细介绍了MySQL MVCC的实现原理和应用场景,希望能帮助读者深入理解这一重要的数据库技术。

#mvcc##mysql#
全部评论

相关推荐

07-23 19:31
武汉大学 Java
1.&nbsp;自我介绍2.&nbsp;两段开源经历拷打,主要聊开发过程遇到的事,技术涉及较少,虽然也没什么技术,估计就是确认一下是楼主干的。3.&nbsp;面试官介绍自己部门不是搞数据库内核的,询问楼主真想来吗。楼主当然不会告诉面试官楼主快star一面挂,给予了肯定的回答。4.&nbsp;开发习惯闲聊,看不看火焰图,跨语言的benchmark怎么测的巴拉巴拉。5.&nbsp;正式开始拷打,汗流浃背了。楼主项目就是常规15445+tinykv,遇到一个也都做过的面试官相当正常。6.&nbsp;15445&nbsp;lru-k算法、crabbing&nbsp;协议(还包括读写锁细节,楼主都快记不得了,头一次有面试官问这个)。7.&nbsp;ACID&nbsp;含义(楼主顺便聊了一下CAP的C跟ACID的C区别,问就是牛客面经,直接预判面试官)8.&nbsp;15445&nbsp;三种隔离级别(RU,&nbsp;RC,&nbsp;RR,这块楼主早忘记了,所以回答的是mysql和pg的实现细节,参考:https://gg2002.github.io/2025/03/16/mysql-latch,顺便扯了几嘴mysql为啥会有表级锁和binlog,因为mysql是一个分离式的架构巴拉巴拉)9.&nbsp;tinykv拷打,multi&nbsp;raft必要性,项目思想。10.&nbsp;分布式事务Percolator跟寻常单体数据库事务的差别(楼主大败而归,说到3列,但是忘记怎么具体地写这3列)11.&nbsp;raft全流程介绍(leader&nbsp;election+log&nbsp;replication,楼主顺便加了点行业现状试图展示知识面)12.&nbsp;raft脑裂问题,prevote优化介绍13.&nbsp;raft的Leader&nbsp;Lease和ReadIndex优化(更是大败而归,头一次有面试官问这个,早就忘记了,扯了几嘴思想草草而过)14.&nbsp;面试官询问tinysql,楼主没做过,但楼主打过ob数据库比赛,说那个比赛sql写的多,再次跟面试官闲聊一阵15.&nbsp;广告场景题,感觉面试官也没想好,反正说了好几分钟背景,然后问楼主广告曝光log和点击log哪个存kv好些,楼主说点击log少些,存点击,面试官说错,然后解释,然后楼主顺着扯了几嘴,然后就过了16.&nbsp;算法题,线程安全的LRU,允许使用cppreference和大模型,面试官也是被楼主思想精通但是stl操作几乎全忘的操作惊到了,不过这次面试官很有耐心,楼主磕磕绊绊地还是写出来了差不多的东西(不过因为ide只有gcc8,楼主的一堆写法还是没过编译)17.&nbsp;此时面了1h20min了,面试官说问点简单的c++八股。18.&nbsp;构造函数、析构函数和虚函数,使用场景(顺便提一嘴shared_ptr包裹的base指针就算析构函数不是虚函数也会调用相应的析构函数,原理是deleter)19.&nbsp;static&nbsp;用法(楼主又忘了,扯了一通const、数据区巴拉巴拉的)20.&nbsp;反问环节:业务:广告取数,职能在db内核开发与大数据开发之间的一个岗位前后耗时1h40min,要面虚脱了。许愿三面吧。快star面试官说楼主经历做rds就够了,干内核还是差点硬核经历。楼主其实也这么觉得,所以很多很硬核的成员变更、分布式事务细节都没太看(结果就被拷打了,马上开补),rds也挺好的,这个岗看上去也比较合楼主口味,楼主早就不强求内核开发了。
面试问题记录
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务