很多开发者每天都在用Transactional却从未真正理解事务隔离的底层实现。当面试官问“MySQL的RR级别是如何解决幻读的”或者“MVCC到底是怎么工作的”如果你只能回答“通过锁”或者“通过多版本”那还停留在“知其然”的层面。今天我们要深入到InnoDB的行记录结构、Undo Log版本链和Read View的生成逻辑彻底拆解MVCC的“平行宇宙”机制看看它是如何实现“读不阻塞写”的。事务隔离的痛点锁的代价在没有MVCC之前数据库要实现隔离性只能靠锁。读操作加共享锁S锁读的时候别人不能写。写操作加排他锁X锁写的时候别人不能读。这种机制在高并发下是灾难性的。想象一下电商系统的商品详情页高频读和库存扣减高频写如果读和写互相阻塞系统吞吐量会直线下降。MVCC的出现就是为了解决这个问题让读操作不加锁让写操作不阻塞读。MVCC的底层解剖行记录的“隐藏字段”在InnoDB中每一行数据聚簇索引记录除了我们定义的列还隐式包含三个隐藏字段DB_TRX_ID6字节最近修改该行数据的事务ID。DB_ROLL_PTR7字节回滚指针指向该行数据在Undo Log中的上一个版本。DB_ROW_ID6字节行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会将旧版本数据写入Undo Log。更新当前行的DB_TRX_ID为当前事务ID。更新DB_ROLL_PTR指向Undo Log中的旧版本。这样所有版本的数据通过DB_ROLL_PTR串联成一个版本链。Undo Log后悔药与版本链Undo Log不仅仅是用来回滚的它还是MVCC的“历史档案馆”。版本链的形成假设初始数据是id1, nameAlice事务ID为100。事务200执行UPDATE user SET nameBob WHERE id1将nameAlice写入Undo Log。更新当前行nameBobDB_TRX_ID200DB_ROLL_PTR指向Undo Log中的Alice版本。事务300执行UPDATE user SET nameCharlie WHERE id1将nameBob写入Undo Log。更新当前行nameCharlieDB_TRX_ID300DB_ROLL_PTR指向Undo Log中的Bob版本。当前行: nameCharlie, DB_TRX_ID300, DB_ROLL_PTR - Undo Log | Undo Log: nameBob, DB_TRX_ID200, DB_ROLL_PTR - Undo Log | Undo Log: nameAlice, DB_TRX_ID100, DB_ROLL_PTR - NULLRead View判断版本可见性的“时光机”这是不是挺难的没关系、其实很好理解下面咱们来看一下有了版本链事务如何判断哪个版本对自己是可见的这就需要一个Read View读视图。struct ReadView { vectoruint64_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如果DB_TRX_ID creator_trx_id当前事务自己修改的可见。如果DB_TRX_ID min_trx_id该版本的事务在Read View生成前已提交可见。如果DB_TRX_ID max_trx_id该版本的事务在Read View生成后才启动不可见。如果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与RRRead View生成时机的差异READ COMMITTEDRC每次查询都生成新的Read View事务A启动生成Read View 1。事务B修改数据并提交。事务A再次查询生成新的Read View 2能看到事务B的修改。结果不可重复读。REPEATABLE READRR第一次查询生成Read View后续复用事务A启动第一次查询时生成Read View 1。事务B修改数据并提交。事务A再次查询复用Read View 1看不到事务B的修改。结果可重复读。RR级别下的幻读问题在RR级别下MVCC只能解决“快照读”的幻读普通SELECT。对于“当前读”SELECT ... FOR UPDATE、UPDATE、INSERTInnoDB使用Next-Key Lock间隙锁记录锁来防止幻读。当前读 vs 快照读快照读Snapshot Read普通的SELECT ...语句。基于MVCC读取历史版本。不加锁不阻塞写。当前读Current ReadSELECT ... FOR UPDATE、UPDATE、INSERT、DELETE。读取最新版本加锁。阻塞其他事务的写操作。为什么SELECT ... FOR UPDATE不走MVCC因为它需要获取排他锁必须读取最新数据否则会导致数据不一致。总结MVCC不是真的复制了多份数据而是利用Undo Log版本链 Read View动态构建出的“历史视图”。Undo Log存储历史版本形成版本链。Read View判断版本可见性实现隔离性。隐藏字段连接当前数据和历史版本。最后送师金句“MVCC不是真的复制了多份数据而是利用Undo Log版本链 Read View动态构建出的‘历史视图’。它是高并发下读写并行的核心秘密。”