MVCC 与事务隔离:MySQL 如何实现“读不阻塞写”?

很多开发者每天都在用@Transactional,却从未真正理解事务隔离的底层实现。

  • 当面试官问:"MySQL的RR级别是如何解决幻读的?"或者"MVCC到底是怎么工作的?"如果你只能回答"通过锁"或者"通过多版本",那还停留在"知其然"的层面。

今天,我们要深入到InnoDB的行记录结构、Undo Log版本链和Read View的生成逻辑,彻底拆解MVCC的"平行宇宙"机制,看看它是如何实现"读不阻塞写"的。

事务隔离的痛点:锁的代价

在没有MVCC之前,数据库要实现隔离性,只能靠锁。

  • 读操作加共享锁(S锁):读的时候,别人不能写。
  • 写操作加排他锁(X锁):写的时候,别人不能读。

这种机制在高并发下是灾难性的。想象一下,电商系统的商品详情页(高频读)和库存扣减(高频写),如果读和写互相阻塞,系统吞吐量会直线下降。

MVCC的出现,就是为了解决这个问题:让读操作不加锁,让写操作不阻塞读。

MVCC的底层解剖:行记录的"隐藏字段"

在InnoDB中,每一行数据(聚簇索引记录)除了我们定义的列,还隐式包含三个隐藏字段:

  • DB_TRX_ID(6字节):最近修改该行数据的事务ID。

  • DB_ROLL_PTR(7字节):回滚指针,指向该行数据在Undo Log中的上一个版本。

  • DB_ROW_ID(6字节):行ID,如果没有主键,InnoDB会自动生成。

    // InnoDB行记录的简化结构
    struct row_t {
    // 用户定义的列
    int id;
    char name[20];

    复制代码
      // 隐藏字段
      uint64_t DB_TRX_ID;      // 最后一次修改的事务ID
      uint64_t DB_ROLL_PTR;    // 指向Undo Log中的旧版本
      uint64_t DB_ROW_ID;      // 行ID

    };

当一个事务修改数据时,InnoDB会:

  1. 将旧版本数据写入Undo Log。
  2. 更新当前行的DB_TRX_ID为当前事务ID。
  3. 更新DB_ROLL_PTR指向Undo Log中的旧版本。

这样,所有版本的数据通过DB_ROLL_PTR串联成一个版本链。

Undo Log:后悔药与版本链

Undo Log不仅仅是用来回滚的,它还是MVCC的"历史档案馆"。

版本链的形成

假设初始数据是id=1, name='Alice',事务ID为100。

  1. 事务200执行UPDATE user SET name='Bob' WHERE id=1:

    • 将name='Alice'写入Undo Log。
    • 更新当前行name='Bob',DB_TRX_ID=200,DB_ROLL_PTR指向Undo Log中的Alice版本。
  2. 事务300执行UPDATE user SET name='Charlie' WHERE id=1:

    • 将name='Bob'写入Undo Log。
    • 更新当前行name='Charlie',DB_TRX_ID=300,DB_ROLL_PTR指向Undo Log中的Bob版本。

    当前行: name='Charlie', DB_TRX_ID=300, DB_ROLL_PTR -> Undo Log
    |
    Undo Log: name='Bob', DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log
    |
    Undo Log: name='Alice', DB_TRX_ID=100, DB_ROLL_PTR -> NULL

Read View:判断版本可见性的"时光机"

这是不是挺难的,没关系、其实很好理解,下面咱们来看一下:

有了版本链,事务如何判断哪个版本对自己是可见的?这就需要一个Read View(读视图)。

复制代码
struct ReadView {
    vector<uint64_t> m_ids;      // 生成Read View时,所有活跃(未提交)事务ID集合
    uint64_t min_trx_id;         // m_ids中的最小值
    uint64_t max_trx_id;         // 生成Read View时,下一个将要分配的事务ID
    uint64_t creator_trx_id;     // 创建该Read View的事务ID
};

可见性判断规则

当事务访问某行数据时,从最新版本开始,沿着版本链依次检查每个版本的DB_TRX_ID:

  1. 如果DB_TRX_ID == creator_trx_id:当前事务自己修改的,可见。
  2. 如果DB_TRX_ID < min_trx_id:该版本的事务在Read View生成前已提交,可见。
  3. 如果DB_TRX_ID >= max_trx_id:该版本的事务在Read View生成后才启动,不可见。
  4. 如果min_trx_id <= DB_TRX_ID < max_trx_id:
    • 如果DB_TRX_ID在m_ids中:该版本的事务在Read View生成时未提交,不可见。
    • 如果DB_TRX_ID不在m_ids中:该版本的事务在Read View生成时已提交,可见。

代码模拟:可见性判断

复制代码
public boolean isVisible(RowVersion version, ReadView readView) {
    uint64_t trxId = version.getDB_TRX_ID();
    
    if (trxId == readView.getCreatorTrxId()) {
        return true; // 自己修改的,可见
    }
    
    if (trxId < readView.getMinTrxId()) {
        return true; // 已提交,可见
    }
    
    if (trxId >= readView.getMaxTrxId()) {
        return false; // 未启动,不可见
    }
    
    // 在min和max之间,检查是否在活跃事务列表中
    return !readView.getMIds().contains(trxId);
}

RC与RR:Read View生成时机的差异

READ COMMITTED(RC):每次查询都生成新的Read View

  • 事务A启动,生成Read View 1。
  • 事务B修改数据并提交。
  • 事务A再次查询,生成新的Read View 2,能看到事务B的修改。
  • 结果:不可重复读。

REPEATABLE READ(RR):第一次查询生成Read View,后续复用

  • 事务A启动,第一次查询时生成Read View 1。
  • 事务B修改数据并提交。
  • 事务A再次查询,复用Read View 1,看不到事务B的修改。
  • 结果:可重复读。

RR级别下的幻读问题

在RR级别下,MVCC只能解决"快照读"的幻读(普通SELECT)。

对于"当前读"(SELECT ... FOR UPDATE、UPDATE、INSERT),InnoDB使用Next-Key Lock(间隙锁+记录锁)来防止幻读。

当前读 vs 快照读

快照读(Snapshot Read)

  • 普通的SELECT ...语句。
  • 基于MVCC,读取历史版本。
  • 不加锁,不阻塞写。

当前读(Current Read)

  • SELECT ... FOR UPDATE、UPDATE、INSERT、DELETE。
  • 读取最新版本,加锁。
  • 阻塞其他事务的写操作。

为什么SELECT ... FOR UPDATE不走MVCC?

因为它需要获取排他锁,必须读取最新数据,否则会导致数据不一致。

总结

MVCC不是真的复制了多份数据,而是利用Undo Log版本链 + Read View动态构建出的"历史视图"。

  • Undo Log:存储历史版本,形成版本链。
  • Read View:判断版本可见性,实现隔离性。
  • 隐藏字段:连接当前数据和历史版本。

最后,送师金句 :

"MVCC不是真的复制了多份数据,而是利用Undo Log版本链 + Read View动态构建出的'历史视图'。它是高并发下读写并行的核心秘密。"

相关推荐
泡茶喝茶写代码11 小时前
A股量化数据工程:从 REST 接口到策略信号(第 8 篇):成长能力因子:营收与利润增速
java·python·股票数据api·股票数据api接口·股票量化数据api·股票量化数据接口·股票数据api数据
hasty12 小时前
policy.xml 在,不代表策略生效:ImageMagick 补丁给安全配置验收的提醒
xml·mysql·安全
Seraphina3612 小时前
SQL注入(二)盲注
网络·数据库·经验分享·笔记·sql
\光辉岁月/12 小时前
7.javase-面向对象
java·开发语言
weixin_5310946912 小时前
heap_4内存管理
java·开发语言·算法
程序员南飞12 小时前
win系统安装minio
java·数据结构·算法
Dawson Zhu12 小时前
《Agentic Design Patterns》第 1 章导读:提示词链(Prompt Chaining)
人工智能·语言模型·架构·aigc·agi
Escalating_xu13 小时前
【Python】使用库:标准库、第三方库、pip、项目实战与程序打包
java·python·pip
feiyu_gao13 小时前
Mindcraft:从个人实践到可复用模式集
架构·aigc·ai编程
abigalexy13 小时前
Claude Code从零搭建新项目全流程AI实现-宠物生命周期管理App
架构·aigc·ai编程