
在MySQL的日常使用中,我们几乎每天都在和事务隔离、并发读写打交道。当多个事务同时操作同一行数据时,为什么有的场景会出现脏读、不可重复读,有的场景却能保证数据一致性?为什么InnoDB能在高并发场景下保持远超其他存储引擎的读写性能?这一切的核心,都离不开InnoDB的多版本并发控制机制——MVCC。
很多开发者对MVCC的认知停留在“读写不互斥”的表层,却对其底层的undo log版本链、Read View可见性规则、不同隔离级别下的行为差异一知半解,甚至被大量错误的博客内容误导,最终在生产环境中遇到数据一致性问题、长事务导致的磁盘爆满等故障时无从下手。
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。其核心思想是:通过维护数据行的多个历史版本,让不同事务的读写操作互不阻塞,在保证数据一致性的前提下,最大化提升数据库的并发性能。
在传统的悲观锁机制中,为了避免并发读写带来的数据不一致问题,会采用“读加共享锁、写加排他锁”的模式,这就导致读写操作之间会互相阻塞,高并发场景下性能急剧下降。
而MVCC彻底打破了这个限制:
这就实现了读写无互斥,读不会阻塞写,写也不会阻塞读,这也是InnoDB能成为MySQL默认存储引擎的核心原因之一。
同时需要明确:InnoDB的MVCC仅在READ COMMITTED(读已提交)和REPEATABLE READ(可重复读)两个隔离级别下生效。READ UNCOMMITTED(读未提交)总是读取最新的数据行版本,无需MVCC;SERIALIZABLE(串行化)则对所有读操作加共享锁,完全靠锁机制保证一致性,也不使用MVCC。
MVCC的核心是多版本,而多版本的载体,就是undo log。很多人对undo log的认知仅停留在“事务回滚日志”,但它更是MVCC版本链的核心载体,没有undo log,就没有MVCC。
要理解undo log的版本链,首先要搞清楚InnoDB聚簇索引行记录的隐藏结构。我们创建的每一行数据,除了我们定义的字段,InnoDB会默认添加3个隐藏列:
隐藏列名 | 长度 | 核心作用 |
|---|---|---|
DB_TRX_ID | 6字节 | 最后一次修改该行记录的事务ID,事务ID是全局递增的,唯一标识一个读写事务 |
DB_ROLL_PTR | 7字节 | 回滚指针,指向该行记录对应的undo log日志,用于构建版本链和事务回滚 |
DB_ROW_ID | 6字节 | 隐藏主键,当表没有定义主键、也没有唯一非空索引时,InnoDB会自动生成 |
这里有两个关键的认知点必须明确:
undo log是逻辑日志,记录的是数据行的修改逻辑,而非物理页的修改。根据操作类型,undo log分为两类:
每一次对数据行的修改,都会生成一条对应的undo log,通过行记录的DB_ROLL_PTR回滚指针,将所有历史版本串联起来,形成一条单向的版本链。
我们通过一个完整的SQL示例,直观展示版本链的生成过程:
-- 创建测试表
CREATETABLE`user` (
`id`INTNOTNULL AUTO_INCREMENT COMMENT'主键ID',
`name`VARCHAR(32) NOTNULLCOMMENT'姓名',
`age`INTNOTNULLCOMMENT'年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户表';
-- 插入初始数据,事务ID=3
INSERTINTO`user` (`id`, `name`, `age`) VALUES (1, '张三', 20);
此时,行记录的隐藏列与undo log如下:
接下来,执行第一次UPDATE操作,事务ID=5:
-- 事务ID=5
UPDATE `user` SET `age`=21 WHERE `id`=1;
此时,InnoDB会做两件事:
然后,执行第二次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的可见性规则,从版本链中找到自己能看到的那个版本,实现不同事务看到不同的数据版本,且完全无需加锁。
如果说undo log版本链是MVCC的“数据载体”,那么Read View就是MVCC的“规则核心”。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。这个错误认知会直接导致可见性判断规则完全错误,必须牢记。
有了Read View和版本链,InnoDB会按照固定的规则,从版本链的最新版本开始,依次判断每个版本是否对当前事务可见,直到找到第一个符合规则的版本为止。
完整的可见性判断规则如下,优先级从高到低:
整个判断流程可以用如下流程图清晰展示:

我们用一个简单的示例验证这个规则: 假设当前Read View的字段如下:
对于版本链中的三个版本:
最终当前事务会读取到DB_TRX_ID=3的版本,完全符合规则。
Read View的生成时机,直接决定了MVCC在不同隔离级别下的行为差异,也是READ COMMITTED和REPEATABLE READ两个隔离级别的核心区别。
InnoDB对两个隔离级别的Read View生成时机做了完全不同的定义:
这个差异,直接导致了两个隔离级别的一致性能力完全不同:
生成时机的差异可以用如下流程图展示:

前面讲完了核心原理,现在我们通过可复现的SQL示例,直观展示不同隔离级别下MVCC的行为差异,所有示例均可在MySQL 8.0中直接执行。
首先,我们先初始化测试数据:
-- 重置测试表
DROPTABLEIFEXISTS`user`;
CREATETABLE`user` (
`id`INTNOTNULL AUTO_INCREMENT COMMENT'主键ID',
`name`VARCHAR(32) NOTNULLCOMMENT'姓名',
`age`INTNOTNULLCOMMENT'年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8mb4 COMMENT='用户表';
-- 插入初始数据
INSERTINTO`user` (`id`, `name`, `age`) VALUES (1, '张三', 20);
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未提交的修改,出现了脏读,这是生产环境绝对禁止使用该隔离级别的核心原因。
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; |
核心原理:
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; |
核心原理:
接下来演示幻读的解决: 很多人认为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; |
核心原理:
SERIALIZABLE级别下,InnoDB不使用MVCC,所有的SELECT语句都会自动加上FOR SHARE共享锁,读写操作互相阻塞,完全串行化执行,彻底解决了脏读、不可重复读、幻读,但并发性能极差,仅适用于对数据一致性要求极高、并发量极低的场景。
行业内关于MVCC的错误认知非常多,这里我们逐一纠正,确保你对MVCC的理解100%准确。
纠正:MVCC仅解决了快照读的幻读问题。对于当前读(SELECT ... FOR UPDATE/LOCK IN SHARE MODE、INSERT、UPDATE、DELETE),MVCC无法解决幻读,InnoDB是通过Next-Key Lock(临键锁)来解决当前读的幻读问题的。
纠正:max_trx_id是生成Read View时,数据库全局下一个要分配的事务ID,而非活跃事务的最大值。这个错误会直接导致可见性判断规则完全错误,是最常见的认知误区。
纠正:InnoDB只会给执行了修改操作的读写事务分配事务ID,纯只读事务不会分配事务ID。且事务ID的分配时机是事务第一次执行修改操作时,而非BEGIN执行时。
纠正:undo log是逻辑日志,用于事务回滚和MVCC版本链的构建;redo log是物理日志,用于数据库崩溃恢复,保证事务的持久性。两者的作用、存储结构、生命周期完全不同,不能混为一谈。
纠正:长事务是MVCC的天敌。长事务会长期持有Read View,导致对应的undo log无法被purge线程清理,不仅会占用大量的磁盘空间,还会导致版本链越来越长,后续的SELECT查询需要遍历更多的版本,查询性能急剧下降。生产环境中必须严格避免长事务。
我们通过Spring Boot + MyBatis-Plus的实战代码,演示不同隔离级别下MVCC的行为差异。
<?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>
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 = "用户实体")
publicclass User implements Serializable {
@Serial
privatestaticfinallong 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;
}
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> {
}
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
publicclass 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);
thrownew 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);
thrownew RuntimeException(e);
}
return user2;
}
/**
* 根据用户ID查询用户信息
* @param userId 用户ID
* @return 用户实体
*/
private User getUserById(Integer userId) {
if (ObjectUtils.isEmpty(userId)) {
thrownew IllegalArgumentException("用户ID不能为空");
}
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<User>()
.eq(User::getId, userId);
return userMapper.selectOne(queryWrapper);
}
}
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的行为")
publicclass 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);
}
}
测试方法:
基于MVCC的底层原理,我们总结了生产环境中的核心最佳实践,帮助你规避常见的坑,提升数据库性能与稳定性。
MVCC是InnoDB的核心灵魂,也是MySQL能支撑高并发场景的核心能力。本文从行记录的隐藏列出发,逐层拆解了undo log版本链的生成、Read View的可见性规则、不同隔离级别下的行为差异,纠正了行业内普遍存在的认知误区,配合可复现的SQL示例与Java实战代码,完整还原了MVCC的底层实现原理。