前言
业务中90%的数据不一致、死锁、并发异常问题,根源都在于对InnoDB事务隔离级别的底层实现理解不到位。本文从底层基石出发,彻底拆解InnoDB事务隔离的核心原理,重点对比RC(读已提交)与RR(可重复读)两大常用隔离级别的核心差异,配合可复现的实例,让你彻底搞懂底层逻辑,不再踩坑。
一、SQL标准隔离级别与读异常基础
1.1 三大读异常定义
- 脏读:事务A读取了事务B未提交的修改数据,若B回滚,A读取的数据即为脏数据。
- 不可重复读:同一个事务内,两次相同的主键查询,返回了不同的行内容(行数据被修改)。
- 幻读:同一个事务内,两次相同的范围查询,第二次返回了第一次没有的行(新增/删除行导致行数变化)。
1.2 SQL标准4个隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 允许 | 允许 | 允许 |
| 读已提交(READ COMMITTED,RC) | 禁止 | 允许 | 允许 |
| 可重复读(REPEATABLE READ,RR) | 禁止 | 禁止 | 允许 |
| 串行化(SERIALIZABLE) | 禁止 | 禁止 | 禁止 |
1.3 InnoDB对标准的实现差异
InnoDB默认隔离级别为RR,且在RR级别下,通过临键锁解决了SQL标准中允许的幻读问题,实现了更强的ACID隔离性。InnoDB的隔离级别实现,完全基于锁机制与MVCC(多版本并发控制) 两大核心基石,下面先拆解这两大核心组件。
二、InnoDB事务隔离的两大核心基石
2.1 锁机制:并发控制的核心屏障
InnoDB实现了行级锁与表级锁,核心锁类型与算法如下:
2.1.1 基础锁类型
- 共享锁(S锁) :读锁,多个事务可同时加S锁,互斥X锁。语法:
SELECT ... LOCK IN SHARE MODE - 排他锁(X锁) :写锁,同一时间只有一个事务可加X锁,互斥所有S/X锁。语法:
SELECT ... FOR UPDATE,UPDATE/DELETE/INSERT会自动加X锁 - 意向锁(IS/IX) :表级锁,用于快速判断表内是否存在行锁,避免全表扫描判断锁冲突。加行S/X锁前,必须先加对应的表级IS/IX锁,意向锁之间互相兼容,仅与表级S/X锁互斥。
2.1.2 行锁算法(核心)
InnoDB的行锁是加在索引上的,无有效索引会退化为表锁,核心行锁算法分为3种:
- 记录锁(Record Lock) :仅锁住索引中的某一行记录,仅针对存在的记录生效。
- 间隙锁(Gap Lock) :锁住索引记录之间的间隙,不锁记录本身,唯一作用是防止其他事务在间隙中插入数据,解决幻读。间隙锁之间互相兼容,仅与插入操作互斥。
- 临键锁(Next-Key Lock) :InnoDB RR级别默认的行锁算法,是记录锁+间隙锁的组合,锁住一个左开右闭的索引区间,彻底杜绝间隙插入,解决幻读。
2.1.3 临键锁的区间规则
InnoDB会将索引按照值排序,划分成多个左开右闭的区间,例如索引列有值10、20、30,会划分出4个临键区间: (-∞,10]、(10,20]、(20,30]、(30,+∞)
临键锁的退化规则(仅RR级别生效):
- 唯一索引的等值查询,且记录存在:临键锁退化为记录锁,仅锁住目标行。
- 唯一索引的等值查询,且记录不存在:临键锁退化为间隙锁,锁住目标值所在的间隙。
2.2 MVCC:无锁并发控制的核心
MVCC(多版本并发控制),是InnoDB实现快照读的核心,通过数据的多版本链,让读操作不加锁,极大提升并发性能。
2.2.1 MVCC的底层依赖
MVCC完全依赖于聚簇索引的隐藏列、undo log版本链、Read View可见性判断三大组件。
1. 聚簇索引的隐藏列
InnoDB聚簇索引的每行数据,都包含3个隐藏列:
trx_id:6字节,最后一次修改该行的事务ID(仅修改数据的事务会分配唯一递增的事务ID,只读事务不分配,trx_id为0)。roll_pointer:7字节,回滚指针,指向该行对应的undo log记录,通过undo log构建数据的版本链。DB_ROW_ID:6字节,隐藏主键,仅当表没有定义主键时生成,用于构建聚簇索引。
2. undo log版本链
每次对数据进行修改时,InnoDB都会生成一条undo log记录:
- 插入操作:生成
insert undo log,事务提交后可直接删除。 - 修改/删除操作:生成
update undo log,记录修改前的数据版本,用于事务回滚和MVCC的版本链构建,必须等到所有需要该版本的事务都提交后,才能被purge线程删除。
通过roll_pointer指针,所有历史版本的数据会形成一条单向链表,即版本链。版本链的头节点是当前最新的数据版本,尾节点是最早的历史版本。
3. Read View:可见性判断的核心
Read View是事务执行快照读时,生成的一个数据快照,记录了当前数据库中活跃的(未提交)事务信息,用于判断版本链中的哪个数据版本对当前事务可见。
Read View包含4个核心字段:
m_ids:生成Read View时,数据库中所有活跃的读写事务ID列表。min_trx_id:m_ids中最小的事务ID,即当前活跃事务的最小ID。max_trx_id:生成Read View时,数据库将要分配的下一个事务ID,即全局最大事务ID+1。creator_trx_id:生成该Read View的当前事务ID。
版本可见性判断规则: 对于版本链中的某个数据版本,trx_id为修改该版本的事务ID:
- 若
trx_id == creator_trx_id:可见,当前事务自己修改的数据。 - 若
trx_id < min_trx_id:可见,修改该版本的事务在Read View生成前已经提交。 - 若
trx_id >= max_trx_id:不可见,修改该版本的事务在Read View生成后才开启。 - 若
min_trx_id <= trx_id < max_trx_id:若trx_id不在m_ids中,可见(事务已提交);若在m_ids中,不可见(事务未提交)。
若当前版本不可见,就顺着roll_pointer找到下一个历史版本,重复上述判断,直到找到可见的版本,或者遍历完版本链返回空。
2.2.2 快照读与当前读
MVCC仅对快照读生效,两种读模式的定义:
- 快照读:普通的
SELECT语句,不加锁,基于MVCC读取数据的可见版本,无锁并发,性能极高。 - 当前读:
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE、INSERT,读取数据的最新提交版本,并且对读取的记录加锁,基于锁机制实现并发控制。
三、RC与RR隔离级别的核心区别(终极拆解)
我们从锁机制、MVCC实现、幻读处理三个核心维度,彻底拆解RC与RR的区别,每个维度都配合可复现的实例。
3.1 锁机制的核心区别
RC与RR在锁机制上的差异,直接决定了两者的并发性能、死锁概率,核心差异有3点:
| 对比维度 | RC隔离级别 | RR隔离级别 |
|---|---|---|
| 行锁算法 | 仅支持记录锁,无间隙锁、临键锁 | 默认使用临键锁,符合条件时退化为记录锁/间隙锁 |
| 锁释放时机 | 不满足查询条件的行,语句执行完立即释放锁,无需等待事务提交 | 所有加锁的记录,必须等待事务提交/回滚后才释放 |
| 半一致性读 | 支持 | 不支持 |
3.1.1 行锁算法差异实例
准备工作:执行以下SQL创建测试表与数据,MySQL8.0环境可直接执行
CREATE TABLE `test_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`age` int NOT NULL COMMENT '年龄',
`name` varchar(32) NOT NULL COMMENT '姓名',
PRIMARY KEY (`id`),
KEY `idx_age` (`age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT '测试用户表';
INSERT INTO `test_user` (`id`, `age`, `name`) VALUES (10, 10, '张三'), (20, 20, '李四'), (30, 30, '王五');
实例1:RC级别下的行锁范围 步骤1:开启会话A,设置RC隔离级别,开启事务,执行带锁的范围查询
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM test_user WHERE age BETWEEN 10 AND 20 FOR UPDATE;
步骤2:开启会话B,执行插入操作,可正常执行,无阻塞
INSERT INTO test_user (id, age, name) VALUES (15, 15, '赵六');
原理:RC级别下,仅对age=10、age=20的两条记录加记录锁,不会锁住(10,20)的间隙,所以会话B可以正常插入age=15的记录,无锁冲突。
实例2:RR级别下的行锁范围 步骤1:开启会话A,设置RR隔离级别,开启事务,执行相同的带锁范围查询
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM test_user WHERE age BETWEEN 10 AND 20 FOR UPDATE;
步骤2:开启会话B,执行相同的插入操作,会被阻塞
INSERT INTO test_user (id, age, name) VALUES (15, 15, '赵六');
原理:RR级别下,InnoDB会对age BETWEEN 10 AND 20的范围加临键锁,锁住(-∞,10]、(10,20]、(20,30)的区间,包括间隙,所以会话B插入age=15的记录,会触发间隙锁冲突,被阻塞,直到会话A提交事务。
3.1.2 锁释放时机与半一致性读差异
半一致性读:RC级别下,UPDATE语句执行时,若遇到已经加了X锁的记录,InnoDB会先读取该记录的最新提交版本,判断是否符合UPDATE的WHERE条件,若不符合,就跳过该记录,不加锁;若符合,才会加锁等待。RR级别不支持半一致性读,遇到加锁的记录,直接加锁等待。
实例3:RC与RR的锁冲突概率对比 步骤1:初始化数据,恢复test_user表的初始数据
TRUNCATE TABLE test_user;
INSERT INTO `test_user` (`id`, `age`, `name`) VALUES (10, 10, '张三'), (20, 20, '李四'), (30, 30, '王五');
步骤2:开启会话A,设置RC隔离级别,开启事务,执行UPDATE语句
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE test_user SET name='张三更新' WHERE age=10;
步骤3:开启会话B,设置RC隔离级别,开启事务,执行UPDATE语句,可正常执行,无阻塞
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE test_user SET name='李四更新' WHERE age=20;
COMMIT;
原理:RC级别下,会话B的UPDATE语句扫描到age=10的记录时,发现已经加了X锁,通过半一致性读读取最新提交版本,判断age=10不符合WHERE条件,直接跳过,不加锁,仅对age=20的记录加锁,所以无冲突。
若将两个会话的隔离级别改为RR,步骤3的UPDATE语句会被阻塞,因为RR级别不支持半一致性读,会话B扫描到age=10的记录时,不管是否符合条件,都会加X锁等待,直到会话A提交事务。
3.2 MVCC实现的核心区别
RC与RR的MVCC实现,可见性判断规则完全一致,核心差异在于Read View的生成时机,这也是不可重复读问题的根源。
| 隔离级别 | Read View生成时机 |
|---|---|
| RC | 事务中每次执行快照读(普通SELECT) 时,都会重新生成一个全新的Read View |
| RR | 事务中第一次执行快照读(普通SELECT) 时,生成一个Read View,整个事务生命周期内复用该Read View |
3.2.1 不可重复读的实例验证
实例4:RC级别下的不可重复读 步骤1:初始化数据,恢复test_user表初始数据
TRUNCATE TABLE test_user;
INSERT INTO `test_user` (`id`, `age`, `name`) VALUES (10, 10, '张三'), (20, 20, '李四'), (30, 30, '王五');
步骤2:开启会话A,设置RC隔离级别,开启事务,执行第一次快照读
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM test_user WHERE id=10;
执行结果:id=10,age=10,name=张三
步骤3:开启会话B,更新id=10的记录,提交事务
BEGIN;
UPDATE test_user SET age=11 WHERE id=10;
COMMIT;
步骤4:会话A执行第二次相同的快照读
SELECT * FROM test_user WHERE id=10;
执行结果:id=10,age=11,name=张三,出现不可重复读。
原理:RC级别下,会话A的两次SELECT,都生成了新的Read View。第二次生成Read View时,会话B的事务已经提交,所以会话B修改的版本对会话A可见,导致两次查询结果不一致。
实例5:RR级别下的可重复读保证 步骤1:初始化数据,恢复test_user表初始数据 步骤2:开启会话A,设置RR隔离级别,开启事务,执行第一次快照读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM test_user WHERE id=10;
执行结果:id=10,age=10,name=张三
步骤3:开启会话B,更新id=10的记录,提交事务
BEGIN;
UPDATE test_user SET age=11 WHERE id=10;
COMMIT;
步骤4:会话A执行第二次相同的快照读
SELECT * FROM test_user WHERE id=10;
执行结果:id=10,age=10,name=张三,实现了可重复读。
原理:RR级别下,会话A第一次SELECT时生成了Read View,整个事务内复用该Read View。会话B的事务是在Read View生成之后提交的,所以修改的版本对会话A不可见,两次查询结果完全一致。
3.3 幻读处理的核心区别
首先明确:幻读的核心是范围查询的行数量变化,而非行内容变化,不可重复读针对的是行内容修改,幻读针对的是新增/删除行导致的范围查询结果变化。
RC与RR在幻读处理上的核心差异,分为快照读和当前读两个场景:
| 场景 | RC隔离级别 | RR隔离级别 |
|---|---|---|
| 快照读(普通SELECT) | 每次生成新的Read View,会出现幻读 | 复用第一次的Read View,不会出现幻读 |
| 当前读(加锁读/写操作) | 仅记录锁,无间隙锁,会出现幻读 | 临键锁锁住范围与间隙,彻底杜绝幻读 |
3.3.1 幻读的实例验证
实例6:RC级别下当前读的幻读 步骤1:初始化数据,恢复test_user表初始数据
TRUNCATE TABLE test_user;
INSERT INTO `test_user` (`id`, `age`, `name`) VALUES (10, 10, '张三'), (20, 20, '李四'), (30, 30, '王五');
步骤2:开启会话A,设置RC隔离级别,开启事务,执行第一次当前读
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM test_user WHERE age BETWEEN 10 AND 30 FOR UPDATE;
执行结果:3条记录,id=10、20、30
步骤3:开启会话B,插入一条符合范围的记录,提交事务
BEGIN;
INSERT INTO test_user (id, age, name) VALUES (25, 25, '赵六');
COMMIT;
步骤4:会话A执行第二次相同的当前读
SELECT * FROM test_user WHERE age BETWEEN 10 AND 30 FOR UPDATE;
执行结果:4条记录,多了id=25的行,出现幻读。
原理:RC级别下,会话A的当前读仅对age=10、20、30的三条记录加记录锁,不会锁住间隙,所以会话B可以插入age=25的记录,导致会话A第二次当前读出现了新的行,触发幻读。
实例7:RR级别下对幻读的彻底解决 步骤1:初始化数据,恢复test_user表初始数据 步骤2:开启会话A,设置RR隔离级别,开启事务,执行第一次当前读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM test_user WHERE age BETWEEN 10 AND 30 FOR UPDATE;
执行结果:3条记录,id=10、20、30
步骤3:开启会话B,执行相同的插入操作,会被阻塞
INSERT INTO test_user (id, age, name) VALUES (25, 25, '赵六');
原理:RR级别下,会话A的当前读会对age BETWEEN 10 AND 30的范围加临键锁,锁住(-∞,10]、(10,20]、(20,30]、(30,+∞)的所有区间,包括间隙,所以会话B插入age=25的记录,会触发间隙锁冲突,被阻塞,直到会话A提交事务,彻底杜绝了幻读。
四、核心流程图与架构图
4.1 Read View可见性判断流程图
4.2 RC与RR的Read View生成时机对比图
4.3 临键锁区间示意图
五、Java实战:基于MyBatis-Plus验证隔离级别差异
5.1 项目依赖配置
pom.xml核心依赖:
<?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.demo</groupId>
<artifactId>innodb-isolation-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>innodb-isolation-demo</name>
<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>
<lombok.version>1.18.32</lombok.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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</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>
5.2 配置文件
application.yml:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
jackson:
default-property-inclusion: non_null
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
springdoc:
swagger-ui:
path: /swagger-ui.html
enabled: true
api-docs:
enabled: true
5.3 实体类
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("test_user")
@Schema(description = "测试用户实体")
public class TestUser implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID", example = "1")
private Long id;
@Schema(description = "年龄", example = "20")
private Integer age;
@Schema(description = "姓名", example = "张三")
private String name;
}
5.4 Mapper接口
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.TestUser;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* 测试用户Mapper接口
* @author ken
*/
public interface TestUserMapper extends BaseMapper<TestUser> {
/**
* 带排他锁的范围查询
* @param minAge 最小年龄
* @param maxAge 最大年龄
* @return 符合条件的用户列表
*/
@Select("SELECT * FROM test_user WHERE age BETWEEN #{minAge} AND #{maxAge} FOR UPDATE")
List<TestUser> selectByAgeRangeForUpdate(@Param("minAge") Integer minAge, @Param("maxAge") Integer maxAge);
/**
* 更新用户年龄
* @param id 用户ID
* @param age 新年龄
* @return 影响行数
*/
@Update("UPDATE test_user SET age = #{age} WHERE id = #{id}")
int updateAgeById(@Param("id") Long id, @Param("age") Integer age);
}
5.5 服务层实现
package com.jam.demo.service;
import com.jam.demo.entity.TestUser;
import com.jam.demo.mapper.TestUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 事务隔离级别测试服务
* @author ken
*/
@Slf4j
@Service
public class IsolationTestService {
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private TestUserMapper testUserMapper;
/**
* 验证RC隔离级别下的不可重复读
* @param userId 用户ID
* @return 两次查询的结果
*/
public String testRcUnrepeatableRead(Long userId) {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
return transactionTemplate.execute(new TransactionCallback<String>() {
@Override
public String doInTransaction(TransactionStatus status) {
log.info("RC事务开启,第一次查询用户ID:{}", userId);
TestUser firstUser = testUserMapper.selectById(userId);
String firstResult = firstUser.toString();
log.info("第一次查询结果:{}", firstResult);
try {
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
try {
log.info("异步线程开启,更新用户年龄");
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.execute(updateStatus -> {
testUserMapper.updateAgeById(userId, firstUser.getAge() + 1);
return 1;
});
log.info("异步线程更新完成,事务提交");
} finally {
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程等待异常", e);
status.setRollbackOnly();
return "执行异常";
}
log.info("RC事务,第二次查询用户ID:{}", userId);
TestUser secondUser = testUserMapper.selectById(userId);
String secondResult = secondUser.toString();
log.info("第二次查询结果:{}", secondResult);
return String.format("第一次查询结果:%s, 第二次查询结果:%s", firstResult, secondResult);
}
});
}
/**
* 验证RR隔离级别下的可重复读
* @param userId 用户ID
* @return 两次查询的结果
*/
public String testRrRepeatableRead(Long userId) {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
return transactionTemplate.execute(new TransactionCallback<String>() {
@Override
public String doInTransaction(TransactionStatus status) {
log.info("RR事务开启,第一次查询用户ID:{}", userId);
TestUser firstUser = testUserMapper.selectById(userId);
String firstResult = firstUser.toString();
log.info("第一次查询结果:{}", firstResult);
try {
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
try {
log.info("异步线程开启,更新用户年龄");
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.execute(updateStatus -> {
testUserMapper.updateAgeById(userId, firstUser.getAge() + 1);
return 1;
});
log.info("异步线程更新完成,事务提交");
} finally {
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程等待异常", e);
status.setRollbackOnly();
return "执行异常";
}
log.info("RR事务,第二次查询用户ID:{}", userId);
TestUser secondUser = testUserMapper.selectById(userId);
String secondResult = secondUser.toString();
log.info("第二次查询结果:{}", secondResult);
return String.format("第一次查询结果:%s, 第二次查询结果:%s", firstResult, secondResult);
}
});
}
/**
* 验证RC隔离级别下的幻读
* @param minAge 最小年龄
* @param maxAge 最大年龄
* @return 两次查询的结果
*/
public String testRcPhantomRead(Integer minAge, Integer maxAge) {
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
return transactionTemplate.execute(new TransactionCallback<String>() {
@Override
public String doInTransaction(TransactionStatus status) {
log.info("RC事务开启,第一次范围查询年龄:{}-{}", minAge, maxAge);
List<TestUser> firstList = testUserMapper.selectByAgeRangeForUpdate(minAge, maxAge);
int firstCount = firstList.size();
log.info("第一次查询数量:{}, 结果:{}", firstCount, firstList);
try {
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
try {
log.info("异步线程开启,插入符合范围的用户");
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.execute(insertStatus -> {
TestUser newUser = new TestUser();
newUser.setAge((minAge + maxAge) / 2);
newUser.setName("新用户");
testUserMapper.insert(newUser);
return 1;
});
log.info("异步线程插入完成,事务提交");
} finally {
countDownLatch.countDown();
}
}).start();
countDownLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程等待异常", e);
status.setRollbackOnly();
return "执行异常";
}
log.info("RC事务,第二次范围查询年龄:{}-{}", minAge, maxAge);
List<TestUser> secondList = testUserMapper.selectByAgeRangeForUpdate(minAge, maxAge);
int secondCount = secondList.size();
log.info("第二次查询数量:{}, 结果:{}", secondCount, secondList);
return String.format("第一次查询数量:%d, 第二次查询数量:%d, 幻读发生:%s",
firstCount, secondCount, firstCount != secondCount);
}
});
}
}
5.6 控制层实现
package com.jam.demo.controller;
import com.jam.demo.service.IsolationTestService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 事务隔离级别测试控制器
* @author ken
*/
@RestController
@RequestMapping("/isolation")
@Tag(name = "事务隔离级别测试接口", description = "验证InnoDB RC与RR隔离级别的核心差异")
public class IsolationTestController {
@Resource
private IsolationTestService isolationTestService;
@GetMapping("/rc/unrepeatable")
@Operation(summary = "验证RC隔离级别下的不可重复读", description = "验证RC隔离级别下,两次相同查询返回不同结果的不可重复读现象")
public String testRcUnrepeatableRead(
@Parameter(description = "用户ID", example = "10", required = true)
@RequestParam Long userId) {
return isolationTestService.testRcUnrepeatableRead(userId);
}
@GetMapping("/rr/repeatable")
@Operation(summary = "验证RR隔离级别下的可重复读", description = "验证RR隔离级别下,两次相同查询返回一致结果的可重复读保证")
public String testRrRepeatableRead(
@Parameter(description = "用户ID", example = "10", required = true)
@RequestParam Long userId) {
return isolationTestService.testRrRepeatableRead(userId);
}
@GetMapping("/rc/phantom")
@Operation(summary = "验证RC隔离级别下的幻读", description = "验证RC隔离级别下,两次相同范围查询返回不同行数的幻读现象")
public String testRcPhantomRead(
@Parameter(description = "最小年龄", example = "10", required = true)
@RequestParam Integer minAge,
@Parameter(description = "最大年龄", example = "30", required = true)
@RequestParam Integer maxAge) {
return isolationTestService.testRcPhantomRead(minAge, maxAge);
}
}
5.7 启动类
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目启动类
* @author ken
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper")
public class InnodbIsolationDemoApplication {
public static void main(String[] args) {
SpringApplication.run(InnodbIsolationDemoApplication.class, args);
}
}
六、业务选型建议与避坑指南
6.1 RC与RR的选型建议
| 选型维度 | 推荐RC隔离级别 | 推荐RR隔离级别 |
|---|---|---|
| 核心诉求 | 高并发、低死锁概率、极致性能 | 强数据一致性、低业务复杂度 |
| 业务场景 | 互联网电商、社交、内容平台等绝大多数OLTP场景 | 金融支付、账务系统、库存管理等对数据一致性要求极高的场景 |
| binlog格式 | 必须使用ROW格式,避免主从不一致 | STATEMENT/ROW格式均可 |
6.2 高频避坑指南
- RR级别下,无索引的更新会导致全表锁 若UPDATE/DELETE的WHERE条件没有有效索引,InnoDB无法定位到具体的行,会对全表所有记录加临键锁,整个表无法插入任何数据,并发完全阻塞,生产环境绝对禁止。
- RC级别下,范围查询的加锁无法防止幻读 若业务需要在RC级别下保证范围查询的一致性,必须手动加锁,且接受并发下降的代价,否则会出现幻读导致的数据不一致。
- RR级别下,事务中过早的快照读会导致数据版本过旧 RR级别下,第一次快照读生成的Read View会被整个事务复用,若事务开启后很久才执行业务操作,会导致读取到的数据是很久之前的版本,引发业务逻辑错误,建议RR级别下,事务开启后立即执行第一次快照读,且事务尽量短小。
- 不要混用快照读与当前读 同一个事务内,若先执行快照读,再执行当前读,当前读会读取最新的提交版本,可能导致快照读与当前读的结果不一致,引发业务逻辑混乱,建议同一个事务内,要么全用快照读,要么全用当前读。
七、核心总结
本文从底层原理出发,彻底拆解了InnoDB事务隔离级别的实现,核心结论如下:
- InnoDB的事务隔离,完全基于锁机制与MVCC两大核心基石,锁机制解决当前读的并发控制,MVCC解决快照读的无锁并发。
- RC与RR的核心差异,本质上是锁的粒度与释放时机、Read View的生成时机的差异,这两个差异直接决定了两者的并发性能、一致性保证。
- RC级别并发性能更高,死锁概率更低,是互联网业务的首选;RR级别数据一致性更强,适合金融等强一致性场景。
- InnoDB的RR级别,通过MVCC解决了快照读的幻读,通过临键锁解决了当前读的幻读,实现了比SQL标准更强的隔离性。
所有的并发问题,本质上都是对隔离级别底层实现的理解不到位。只有彻底搞懂底层原理,才能写出高并发、高一致性的业务代码,彻底杜绝数据不一致、死锁等线上问题。