击穿 MySQL InnoDB MVCC 底层:从 undo log、Read View 到隔离级别的全链路深度拆解

简介: 本文深入解析MySQL InnoDB的MVCC机制,涵盖undo log版本链、Read View可见性规则、各隔离级别行为差异,并纠正脏读/幻读等常见误区,辅以SQL与Spring Boot实战演示,助你透彻理解高并发下数据一致性的底层原理。

在MySQL的日常使用中,我们几乎每天都在和事务隔离、并发读写打交道。当多个事务同时操作同一行数据时,为什么有的场景会出现脏读、不可重复读,有的场景却能保证数据一致性?为什么InnoDB能在高并发场景下保持远超其他存储引擎的读写性能?这一切的核心,都离不开InnoDB的多版本并发控制机制——MVCC。

很多开发者对MVCC的认知停留在“读写不互斥”的表层,却对其底层的undo log版本链、Read View可见性规则、不同隔离级别下的行为差异一知半解,甚至被大量错误的博客内容误导,最终在生产环境中遇到数据一致性问题、长事务导致的磁盘爆满等故障时无从下手。

一、MVCC的核心本质:解决什么问题?

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。其核心思想是:通过维护数据行的多个历史版本,让不同事务的读写操作互不阻塞,在保证数据一致性的前提下,最大化提升数据库的并发性能。

在传统的悲观锁机制中,为了避免并发读写带来的数据不一致问题,会采用“读加共享锁、写加排他锁”的模式,这就导致读写操作之间会互相阻塞,高并发场景下性能急剧下降。

而MVCC彻底打破了这个限制:

  • 写操作:不会直接覆盖原数据,而是生成一个新的版本,保留历史版本链
  • 读操作:通过一致性视图Read View,读取符合可见性规则的历史版本,无需加锁

这就实现了读写无互斥,读不会阻塞写,写也不会阻塞读,这也是InnoDB能成为MySQL默认存储引擎的核心原因之一。

同时需要明确:InnoDB的MVCC仅在READ COMMITTED(读已提交)REPEATABLE READ(可重复读)两个隔离级别下生效。READ UNCOMMITTED(读未提交)总是读取最新的数据行版本,无需MVCC;SERIALIZABLE(串行化)则对所有读操作加共享锁,完全靠锁机制保证一致性,也不使用MVCC。

二、MVCC的基石1:undo log与行版本链

MVCC的核心是多版本,而多版本的载体,就是undo log。很多人对undo log的认知仅停留在“事务回滚日志”,但它更是MVCC版本链的核心载体,没有undo log,就没有MVCC。

2.1 InnoDB行记录的隐藏列

要理解undo log的版本链,首先要搞清楚InnoDB聚簇索引行记录的隐藏结构。我们创建的每一行数据,除了我们定义的字段,InnoDB会默认添加3个隐藏列:

隐藏列名 长度 核心作用
DB_TRX_ID 6字节 最后一次修改该行记录的事务ID,事务ID是全局递增的,唯一标识一个读写事务
DB_ROLL_PTR 7字节 回滚指针,指向该行记录对应的undo log日志,用于构建版本链和事务回滚
DB_ROW_ID 6字节 隐藏主键,当表没有定义主键、也没有唯一非空索引时,InnoDB会自动生成

这里有两个关键的认知点必须明确:

  1. 事务ID的分配时机:InnoDB只会给执行了修改操作(INSERT/UPDATE/DELETE)的读写事务分配唯一的、全局递增的事务ID。纯只读事务(只有SELECT语句)不会分配事务ID,其事务ID默认为0。
  2. DELETE操作的本质:InnoDB的DELETE操作并不是物理删除数据行,而是通过修改行记录的delete flag标记位,将其标记为已删除,真正的物理删除会由后台的purge线程在确认没有任何事务需要该版本时执行。

2.2 undo log的类型与生命周期

undo log是逻辑日志,记录的是数据行的修改逻辑,而非物理页的修改。根据操作类型,undo log分为两类:

  1. insert undo log:由INSERT操作生成。INSERT操作插入的记录,只有当前事务能看到,其他事务不会访问该记录的版本,因此事务提交后,对应的insert undo log可以被直接删除,无需purge线程处理。
  2. update undo log:由UPDATE/DELETE操作生成。这类undo log不仅用于事务回滚,还需要为MVCC的一致性读提供历史版本,因此事务提交后不能直接删除,必须等到没有任何事务的Read View需要引用该历史版本时,才会由purge线程统一清理。

2.3 版本链的生成过程

每一次对数据行的修改,都会生成一条对应的undo log,通过行记录的DB_ROLL_PTR回滚指针,将所有历史版本串联起来,形成一条单向的版本链。

我们通过一个完整的SQL示例,直观展示版本链的生成过程:

-- 创建测试表
CREATE TABLE `user` (
 `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `name` VARCHAR(32) NOT NULL COMMENT '姓名',
 `age` INT NOT NULL COMMENT '年龄',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 插入初始数据,事务ID=3
INSERT INTO `user` (`id`, `name`, `age`) VALUES (1, '张三', 20);

此时,行记录的隐藏列与undo log如下:

  • DB_TRX_ID=3
  • DB_ROLL_PTR=null
  • 版本链只有初始版本,无历史undo log

接下来,执行第一次UPDATE操作,事务ID=5:

-- 事务ID=5
UPDATE `user` SET `age`=21 WHERE `id`=1;

此时,InnoDB会做两件事:

  1. 生成一条update undo log,记录修改前的版本(age=20),该undo log的DB_TRX_ID=3
  2. 修改当前行记录的age=21,DB_TRX_ID=5,DB_ROLL_PTR指向刚生成的undo log

然后,执行第二次UPDATE操作,事务ID=8:

-- 事务ID=8
UPDATE `user` SET `age`=22 WHERE `id`=1;

同样,生成新的undo log,记录age=21的版本,DB_TRX_ID=5,当前行的DB_TRX_ID=8,DB_ROLL_PTR指向新的undo log,版本链进一步延伸。

最终形成的版本链结构如下:

这条版本链,就是MVCC实现一致性读的基础。当不同事务执行SELECT操作时,会通过Read View的可见性规则,从版本链中找到自己能看到的那个版本,实现不同事务看到不同的数据版本,且完全无需加锁。

三、MVCC的基石2:Read View一致性视图与可见性规则

如果说undo log版本链是MVCC的“数据载体”,那么Read View就是MVCC的“规则核心”。Read View是事务执行一致性读时生成的一个数据快照,定义了当前事务能看到哪些数据版本,不能看到哪些数据版本。

3.1 Read View的核心字段

Read View的结构由4个核心字段组成,这4个字段是版本可见性判断的唯一依据,这里必须纠正行业内普遍存在的错误认知,所有字段定义均来自MySQL 8.0 InnoDB源码:

字段名 核心含义
m_ids 生成Read View时,当前数据库中所有活跃的读写事务ID集合(已启动但未提交)
min_trx_id m_ids集合中的最小事务ID,即当前未提交事务的最小ID
max_trx_id 生成Read View时,数据库全局下一个要分配的事务ID
creator_trx_id 生成当前Read View的事务的ID,只读事务的creator_trx_id为0

这里最核心的错误纠正:max_trx_id不是活跃事务的最大值,而是全局下一个要分配的事务ID。比如当前活跃的读写事务ID是2、3、5,那么下一个要分配的事务ID是6,max_trx_id=6,而非5。这个错误认知会直接导致可见性判断规则完全错误,必须牢记。

3.2 版本可见性判断规则

有了Read View和版本链,InnoDB会按照固定的规则,从版本链的最新版本开始,依次判断每个版本是否对当前事务可见,直到找到第一个符合规则的版本为止。

完整的可见性判断规则如下,优先级从高到低:

  1. 规则1:如果当前版本的DB_TRX_ID == creator_trx_id → 可见。当前事务自己修改的版本,自己当然可以看到。
  2. 规则2:如果当前版本的DB_TRX_ID < min_trx_id → 可见。该版本对应的事务,在当前Read View生成之前就已经提交,对当前事务可见。
  3. 规则3:如果当前版本的DB_TRX_ID >= max_trx_id → 不可见。该版本对应的事务,是在当前Read View生成之后才启动的,对当前事务不可见,需要回溯到上一个版本。
  4. 规则4:如果当前版本的DB_TRX_ID在min_trx_id和max_trx_id之间,判断DB_TRX_ID是否在m_ids集合中:
  • 如果在m_ids中 → 不可见。该事务在Read View生成时还处于活跃状态,未提交,修改的内容不可见。
  • 如果不在m_ids中 → 可见。该事务在Read View生成时已经提交,修改的内容可见。

整个判断流程可以用如下流程图清晰展示:

我们用一个简单的示例验证这个规则: 假设当前Read View的字段如下:

  • m_ids = [5,8]
  • min_trx_id = 5
  • max_trx_id = 10
  • creator_trx_id = 0(只读事务)

对于版本链中的三个版本:

  1. 版本1:DB_TRX_ID=8 → 处于min和max之间,且在m_ids中 → 不可见
  2. 版本2:DB_TRX_ID=5 → 处于min和max之间,且在m_ids中 → 不可见
  3. 版本3:DB_TRX_ID=3 → 小于min_trx_id=5 → 可见

最终当前事务会读取到DB_TRX_ID=3的版本,完全符合规则。

3.3 Read View的生成时机:隔离级别差异的核心

Read View的生成时机,直接决定了MVCC在不同隔离级别下的行为差异,也是READ COMMITTED和REPEATABLE READ两个隔离级别的核心区别。

InnoDB对两个隔离级别的Read View生成时机做了完全不同的定义:

  1. READ COMMITTED(读已提交)事务内每执行一次SELECT语句,都会生成一个全新的Read View
  2. REPEATABLE READ(可重复读)事务内第一次执行SELECT语句时,生成一个Read View,整个事务生命周期内复用这个Read View

这个差异,直接导致了两个隔离级别的一致性能力完全不同:

  • RC级别:每次SELECT都用最新的Read View,能看到其他事务已经提交的修改,因此会出现不可重复读。
  • RR级别:整个事务复用同一个Read View,后续的SELECT都用同一个规则判断可见性,因此不会看到其他事务提交的修改,解决了不可重复读。

生成时机的差异可以用如下流程图展示:

四、不同隔离级别下MVCC的行为差异实战

前面讲完了核心原理,现在我们通过可复现的SQL示例,直观展示不同隔离级别下MVCC的行为差异,所有示例均可在MySQL 8.0中直接执行。

首先,我们先初始化测试数据:

-- 重置测试表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
 `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `name` VARCHAR(32) NOT NULL COMMENT '姓名',
 `age` INT NOT NULL COMMENT '年龄',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- 插入初始数据
INSERT INTO `user` (`id`, `name`, `age`) VALUES (1, '张三', 20);

4.1 READ UNCOMMITTED:无MVCC,存在脏读

READ UNCOMMITTED级别下,InnoDB不使用MVCC,每次SELECT都会直接读取数据行的最新版本,无论对应的事务是否提交,因此会出现脏读。

示例执行流程(两个会话并行执行,严格按照时间点顺序):

时间点 会话A(读未提交) 会话B(读未提交)
T1 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN; SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; BEGIN;
T2 SELECT * FROM user WHERE id=1; -- 结果:age=20
T3 UPDATE user SET age=21 WHERE id=1; -- 未提交
T4 SELECT * FROM user WHERE id=1; -- 结果:age=21(脏读,读取到未提交的数据)
T5 ROLLBACK;
T6 SELECT * FROM user WHERE id=1; -- 结果:age=20 COMMIT;
T7 COMMIT;

可以看到,会话A在T4时刻读取到了会话B未提交的修改,出现了脏读,这是生产环境绝对禁止使用该隔离级别的核心原因。

4.2 READ COMMITTED:每次SELECT生成新Read View,解决脏读,存在不可重复读

RC级别下,每次SELECT都会生成新的Read View,因此只能看到已经提交的事务修改,解决了脏读,但因为每次Read View都是最新的,会出现不可重复读。

示例执行流程:

时间点 会话A(读已提交) 会话B(读已提交)
T1 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN;
T2 SELECT * FROM user WHERE id=1; -- 结果:age=20
T3 UPDATE user SET age=21 WHERE id=1; -- 未提交
T4 SELECT * FROM user WHERE id=1; -- 结果:age=20(脏读解决,未提交的修改不可见)
T5 COMMIT;
T6 SELECT * FROM user WHERE id=1; -- 结果:age=21(不可重复读,新的Read View看到了已提交的修改)
T7 COMMIT;

核心原理:

  • T2时刻,会话A第一次SELECT生成Read View,此时会话B的事务还未启动,m_ids为空,min_trx_id=当前最大事务ID+1,因此只能看到初始版本age=20。
  • T4时刻,会话B的事务ID在Read View的m_ids中,因此修改的版本不可见,还是读取age=20。
  • T6时刻,会话A再次SELECT,生成全新的Read View,此时会话B的事务已经提交,不在m_ids中,因此能看到age=21的版本,出现不可重复读。

4.3 REPEATABLE READ:复用Read View,解决不可重复读与快照读幻读

RR是InnoDB的默认隔离级别,事务内第一次SELECT生成Read View后,整个事务复用,因此解决了不可重复读,同时配合MVCC解决了快照读的幻读问题。

首先演示不可重复读的解决:

时间点 会话A(可重复读) 会话B(可重复读)
T1 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN;
T2 SELECT * FROM user WHERE id=1; -- 结果:age=20
T3 UPDATE user SET age=21 WHERE id=1; COMMIT;
T4 SELECT * FROM user WHERE id=1; -- 结果:age=20(可重复读,复用第一次的Read View,看不到已提交的修改)
T5 COMMIT;

核心原理:

  • T2时刻,会话A第一次SELECT生成Read View,整个事务生命周期内复用这个Read View。
  • T3时刻,会话B的事务提交,但其事务ID在会话A的Read View的m_ids中,因此根据可见性规则,修改的版本不可见。
  • T4时刻,会话A再次SELECT,还是用同一个Read View,因此还是读取到age=20的版本,实现了可重复读。

接下来演示幻读的解决: 很多人认为MVCC不能解决幻读,这是一个典型的认知误区。InnoDB的RR级别下,快照读的幻读完全由MVCC解决,当前读的幻读由Next-Key Lock解决

示例执行流程:

时间点 会话A(可重复读) 会话B(可重复读)
T1 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN;
T2 SELECT * FROM user WHERE age BETWEEN 20 AND 30; -- 结果:1条记录(id=1,age=20)
T3 INSERT INTO user (name, age) VALUES ('李四', 25); COMMIT;
T4 SELECT * FROM user WHERE age BETWEEN 20 AND 30; -- 结果:1条记录(无幻读,MVCC解决)
T5 SELECT * FROM user WHERE age BETWEEN 20 AND 30 FOR UPDATE; -- 当前读,结果:2条记录
T6 COMMIT;

核心原理:

  • T2时刻,会话A第一次快照读生成Read View,整个事务复用。
  • T3时刻,会话B插入新数据并提交,但其事务ID在会话A的Read View的m_ids中,因此不可见。
  • T4时刻,会话A再次快照读,复用同一个Read View,因此还是只看到1条记录,没有出现幻读。
  • T5时刻,当前读(FOR UPDATE)会读取最新的版本,加Next-Key Lock,因此会看到2条记录,当前读的幻读需要靠锁机制解决。

4.4 SERIALIZABLE:无MVCC,完全串行化

SERIALIZABLE级别下,InnoDB不使用MVCC,所有的SELECT语句都会自动加上FOR SHARE共享锁,读写操作互相阻塞,完全串行化执行,彻底解决了脏读、不可重复读、幻读,但并发性能极差,仅适用于对数据一致性要求极高、并发量极低的场景。

五、MVCC的常见认知误区纠正

行业内关于MVCC的错误认知非常多,这里我们逐一纠正,确保你对MVCC的理解100%准确。

误区1:MVCC完全解决了幻读

纠正:MVCC仅解决了快照读的幻读问题。对于当前读(SELECT ... FOR UPDATE/LOCK IN SHARE MODE、INSERT、UPDATE、DELETE),MVCC无法解决幻读,InnoDB是通过Next-Key Lock(临键锁)来解决当前读的幻读问题的。

误区2:Read View的max_trx_id是活跃事务的最大值

纠正:max_trx_id是生成Read View时,数据库全局下一个要分配的事务ID,而非活跃事务的最大值。这个错误会直接导致可见性判断规则完全错误,是最常见的认知误区。

误区3:事务开启(BEGIN)时就会分配事务ID

纠正:InnoDB只会给执行了修改操作的读写事务分配事务ID,纯只读事务不会分配事务ID。且事务ID的分配时机是事务第一次执行修改操作时,而非BEGIN执行时。

误区4:undo log和redo log都是用来回滚的

纠正:undo log是逻辑日志,用于事务回滚和MVCC版本链的构建;redo log是物理日志,用于数据库崩溃恢复,保证事务的持久性。两者的作用、存储结构、生命周期完全不同,不能混为一谈。

误区5:长事务不影响数据库性能

纠正:长事务是MVCC的天敌。长事务会长期持有Read View,导致对应的undo log无法被purge线程清理,不仅会占用大量的磁盘空间,还会导致版本链越来越长,后续的SELECT查询需要遍历更多的版本,查询性能急剧下降。生产环境中必须严格避免长事务。

六、MVCC实战代码演示

我们通过Spring Boot + MyBatis-Plus的实战代码,演示不同隔离级别下MVCC的行为差异。

6.1 项目核心依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>3.2.5</version>
       <relativePath/>
   </parent>
   <groupId>com.jam</groupId>
   <artifactId>mvcc-demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>mvcc-demo</name>
   <description>MySQL MVCC测试项目</description>
   <properties>
       <java.version>17</java.version>
       <mybatis-plus.version>3.5.7</mybatis-plus.version>
       <fastjson2.version>2.0.52</fastjson2.version>
       <guava.version>33.1.0-jre</guava.version>
       <springdoc.version>2.5.0</springdoc.version>
   </properties>
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-jdbc</artifactId>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>
       <dependency>
           <groupId>org.springdoc</groupId>
           <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
           <version>${springdoc.version}</version>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>1.18.32</version>
           <scope>provided</scope>
       </dependency>
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>${fastjson2.version}</version>
       </dependency>
       <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>${guava.version}</version>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>
   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <configuration>
                   <excludes>
                       <exclude>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                       </exclude>
                   </excludes>
               </configuration>
           </plugin>
       </plugins>
   </build>
</project>

6.2 实体类定义

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serial;
import java.io.Serializable;

/**
* 用户实体类
* @author ken
*/

@Data
@TableName("user")
@Schema(description = "用户实体")
public class User implements Serializable {

   @Serial
   private static final long serialVersionUID = 1L;

   @TableId(type = IdType.AUTO)
   @Schema(description = "主键ID", example = "1")
   private Integer id;

   @Schema(description = "用户姓名", example = "张三")
   private String name;

   @Schema(description = "用户年龄", example = "20")
   private Integer age;
}

6.3 Mapper接口

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
* 用户Mapper接口
* @author ken
*/

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

6.4 服务层接口与实现

package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.User;

/**
* 用户服务接口
* @author ken
*/

public interface UserService extends IService<User> {

   /**
    * 测试RC隔离级别下的MVCC行为
    * @param userId 用户ID
    * @return 用户信息
    */

   User testRcIsolationLevel(Integer userId);

   /**
    * 测试RR隔离级别下的MVCC行为
    * @param userId 用户ID
    * @return 用户信息
    */

   User testRrIsolationLevel(Integer userId);
}

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ObjectUtils;

import jakarta.annotation.Resource;

/**
* 用户服务实现类
* @author ken
*/

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

   @Resource
   private PlatformTransactionManager transactionManager;

   @Resource
   private UserMapper userMapper;

   @Override
   public User testRcIsolationLevel(Integer userId) {
       DefaultTransactionDefinition def = new DefaultTransactionDefinition();
       def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
       def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
       TransactionStatus status = transactionManager.getTransaction(def);

       User user1;
       User user2;
       try {
           user1 = getUserById(userId);
           log.info("RC隔离级别-第一次查询结果:{}", user1);

           Thread.sleep(10000);

           user2 = getUserById(userId);
           log.info("RC隔离级别-第二次查询结果:{}", user2);

           transactionManager.commit(status);
       } catch (Exception e) {
           transactionManager.rollback(status);
           log.error("RC隔离级别测试异常", e);
           throw new RuntimeException(e);
       }
       return user2;
   }

   @Override
   public User testRrIsolationLevel(Integer userId) {
       DefaultTransactionDefinition def = new DefaultTransactionDefinition();
       def.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
       def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
       TransactionStatus status = transactionManager.getTransaction(def);

       User user1;
       User user2;
       try {
           user1 = getUserById(userId);
           log.info("RR隔离级别-第一次查询结果:{}", user1);

           Thread.sleep(10000);

           user2 = getUserById(userId);
           log.info("RR隔离级别-第二次查询结果:{}", user2);

           transactionManager.commit(status);
       } catch (Exception e) {
           transactionManager.rollback(status);
           log.error("RR隔离级别测试异常", e);
           throw new RuntimeException(e);
       }
       return user2;
   }

   /**
    * 根据用户ID查询用户信息
    * @param userId 用户ID
    * @return 用户实体
    */

   private User getUserById(Integer userId) {
       if (ObjectUtils.isEmpty(userId)) {
           throw new IllegalArgumentException("用户ID不能为空");
       }
       LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>()
               .eq(User::getId, userId);
       return userMapper.selectOne(queryWrapper);
   }
}

6.5 接口层定义

package com.jam.demo.controller;

import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.annotation.Resource;

/**
* MVCC测试控制器
* @author ken
*/

@RestController
@RequestMapping("/mvcc")
@Tag(name = "MVCC测试接口", description = "测试不同隔离级别下MVCC的行为")
public class MvccTestController {

   @Resource
   private UserService userService;

   @GetMapping("/test/rc/{userId}")
   @Operation(summary = "测试RC隔离级别", description = "测试READ COMMITTED隔离级别下MVCC的不可重复读现象")
   public User testRcIsolation(
           @Parameter(description = "用户ID", example = "1")
@PathVariable Integer userId) {
       return userService.testRcIsolationLevel(userId);
   }

   @GetMapping("/test/rr/{userId}")
   @Operation(summary = "测试RR隔离级别", description = "测试REPEATABLE READ隔离级别下MVCC的可重复读能力")
   public User testRrIsolation(
           @Parameter(description = "用户ID", example = "1")
@PathVariable Integer userId) {
       return userService.testRrIsolationLevel(userId);
   }
}

测试方法:

  1. 启动Spring Boot项目,访问Swagger地址:http://localhost:8080/swagger-ui.html
  2. 调用/mvcc/test/rc/1接口,在接口等待的10秒内,手动执行UPDATE user SET age=21 WHERE id=1;并提交,观察日志输出,两次查询结果不一致,出现不可重复读。
  3. 调用/mvcc/test/rr/1接口,同样在等待的10秒内执行UPDATE并提交,观察日志输出,两次查询结果一致,实现了可重复读。

七、MVCC的生产最佳实践

基于MVCC的底层原理,我们总结了生产环境中的核心最佳实践,帮助你规避常见的坑,提升数据库性能与稳定性。

  1. 严格控制长事务长事务是MVCC的最大天敌,会导致undo log无法清理,版本链过长,查询性能下降,磁盘空间占用飙升。生产环境中必须避免在事务中执行耗时的非数据库操作,禁止在事务中等待人工操作,合理设置autocommit,避免非显式事务导致的长事务。
  2. 合理选择事务隔离级别对于高并发写、对一致性要求不极致的场景,推荐使用RC隔离级别。RC级别下,undo log的purge更及时,版本链更短,查询性能更好,同时binlog的row格式也能避免主从不一致问题。对于对数据一致性要求高的场景,使用InnoDB默认的RR隔离级别即可。
  3. 避免大事务批量修改数据大批量的UPDATE/DELETE操作会生成大量的undo log,不仅会导致事务执行时间过长,还会导致版本链急剧膨胀,影响其他事务的查询性能。生产环境中,大批量修改操作建议拆分成多个小事务分批执行。
  4. 正确使用索引,避免全表扫描全表扫描会导致InnoDB扫描大量的数据行,遍历每个数据行的版本链,查询性能急剧下降。同时,全表扫描还会导致锁范围过大,影响并发性能。必须为查询条件建立合适的索引。
  5. 定期监控undo log的使用情况生产环境中需要定期监控undo log的大小、增长速度,以及长事务的运行情况。当发现undo log持续增长时,及时定位并处理长事务,避免磁盘空间被占满。

总结

MVCC是InnoDB的核心灵魂,也是MySQL能支撑高并发场景的核心能力。本文从行记录的隐藏列出发,逐层拆解了undo log版本链的生成、Read View的可见性规则、不同隔离级别下的行为差异,纠正了行业内普遍存在的认知误区,配合可复现的SQL示例与Java实战代码,完整还原了MVCC的底层实现原理。

目录
相关文章
|
6天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4357 17
|
17天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
16646 138
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
5天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
4819 8
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
7天前
|
人工智能 自然语言处理 数据挖掘
零基础30分钟搞定 Claude Code,这一步90%的人直接跳过了
本文直击Claude Code使用痛点,提供零基础30分钟上手指南:强调必须配置“工作上下文”(about-me.md+anti-ai-style.md)、采用Cowork/Code模式、建立标准文件结构、用提问式提示词驱动AI理解→规划→执行。附可复制模板与真实项目启动法,助你将Claude从聊天工具升级为高效执行系统。
|
6天前
|
人工智能 定位技术
Claude Code源码泄露:8大隐藏功能曝光
2026年3月,Anthropic因配置失误致Claude Code超51万行源码泄露,意外促成“被动开源”。代码中藏有8大未发布功能,揭示其向“超级智能体”演进的完整蓝图,引发AI编程领域震动。(239字)
2461 9