击穿 InnoDB 事务隔离级别:RC 与 RR 的底层实现、锁机制、MVCC 与幻读终极拆解

0 阅读21分钟

前言

业务中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种:

  1. 记录锁(Record Lock) :仅锁住索引中的某一行记录,仅针对存在的记录生效。
  2. 间隙锁(Gap Lock) :锁住索引记录之间的间隙,不锁记录本身,唯一作用是防止其他事务在间隙中插入数据,解决幻读。间隙锁之间互相兼容,仅与插入操作互斥。
  3. 临键锁(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_idm_ids中最小的事务ID,即当前活跃事务的最小ID。
  • max_trx_id:生成Read View时,数据库将要分配的下一个事务ID,即全局最大事务ID+1。
  • creator_trx_id:生成该Read View的当前事务ID。

版本可见性判断规则: 对于版本链中的某个数据版本,trx_id为修改该版本的事务ID:

  1. trx_id == creator_trx_id:可见,当前事务自己修改的数据。
  2. trx_id < min_trx_id:可见,修改该版本的事务在Read View生成前已经提交。
  3. trx_id >= max_trx_id:不可见,修改该版本的事务在Read View生成后才开启。
  4. min_trx_id <= trx_id < max_trx_id:若trx_id不在m_ids中,可见(事务已提交);若在m_ids中,不可见(事务未提交)。

若当前版本不可见,就顺着roll_pointer找到下一个历史版本,重复上述判断,直到找到可见的版本,或者遍历完版本链返回空。

2.2.2 快照读与当前读

MVCC仅对快照读生效,两种读模式的定义:

  • 快照读:普通的SELECT语句,不加锁,基于MVCC读取数据的可见版本,无锁并发,性能极高。
  • 当前读SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETEINSERT,读取数据的最新提交版本,并且对读取的记录加锁,基于锁机制实现并发控制。

三、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(32NOT 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 (1010'张三'), (2020'李四'), (3030'王五');

实例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 (1515'赵六');

原理: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 (1515'赵六');

原理: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 (1010'张三'), (2020'李四'), (3030'王五');

步骤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 (1010'张三'), (2020'李四'), (3030'王五');

步骤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 (1010'张三'), (2020'李四'), (3030'王五');

步骤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 (2525'赵六');
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 (2525'赵六');

原理: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 高频避坑指南

  1. RR级别下,无索引的更新会导致全表锁 若UPDATE/DELETE的WHERE条件没有有效索引,InnoDB无法定位到具体的行,会对全表所有记录加临键锁,整个表无法插入任何数据,并发完全阻塞,生产环境绝对禁止。
  2. RC级别下,范围查询的加锁无法防止幻读 若业务需要在RC级别下保证范围查询的一致性,必须手动加锁,且接受并发下降的代价,否则会出现幻读导致的数据不一致。
  3. RR级别下,事务中过早的快照读会导致数据版本过旧 RR级别下,第一次快照读生成的Read View会被整个事务复用,若事务开启后很久才执行业务操作,会导致读取到的数据是很久之前的版本,引发业务逻辑错误,建议RR级别下,事务开启后立即执行第一次快照读,且事务尽量短小。
  4. 不要混用快照读与当前读 同一个事务内,若先执行快照读,再执行当前读,当前读会读取最新的提交版本,可能导致快照读与当前读的结果不一致,引发业务逻辑混乱,建议同一个事务内,要么全用快照读,要么全用当前读。

七、核心总结

本文从底层原理出发,彻底拆解了InnoDB事务隔离级别的实现,核心结论如下:

  1. InnoDB的事务隔离,完全基于锁机制MVCC两大核心基石,锁机制解决当前读的并发控制,MVCC解决快照读的无锁并发。
  2. RC与RR的核心差异,本质上是锁的粒度与释放时机Read View的生成时机的差异,这两个差异直接决定了两者的并发性能、一致性保证。
  3. RC级别并发性能更高,死锁概率更低,是互联网业务的首选;RR级别数据一致性更强,适合金融等强一致性场景。
  4. InnoDB的RR级别,通过MVCC解决了快照读的幻读,通过临键锁解决了当前读的幻读,实现了比SQL标准更强的隔离性。

所有的并发问题,本质上都是对隔离级别底层实现的理解不到位。只有彻底搞懂底层原理,才能写出高并发、高一致性的业务代码,彻底杜绝数据不一致、死锁等线上问题。