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

前言

业务中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环境可直接执行

sql 复制代码
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隔离级别,开启事务,执行带锁的范围查询

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM test_user WHERE age BETWEEN 10 AND 20 FOR UPDATE;

步骤2:开启会话B,执行插入操作,可正常执行,无阻塞

sql 复制代码
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隔离级别,开启事务,执行相同的带锁范围查询

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM test_user WHERE age BETWEEN 10 AND 20 FOR UPDATE;

步骤2:开启会话B,执行相同的插入操作,会被阻塞

sql 复制代码
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表的初始数据

sql 复制代码
TRUNCATE TABLE test_user;
INSERT INTO `test_user` (`id`, `age`, `name`) VALUES (10, 10, '张三'), (20, 20, '李四'), (30, 30, '王五');

步骤2:开启会话A,设置RC隔离级别,开启事务,执行UPDATE语句

ini 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
UPDATE test_user SET name='张三更新' WHERE age=10;

步骤3:开启会话B,设置RC隔离级别,开启事务,执行UPDATE语句,可正常执行,无阻塞

ini 复制代码
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表初始数据

sql 复制代码
TRUNCATE TABLE test_user;
INSERT INTO `test_user` (`id`, `age`, `name`) VALUES (10, 10, '张三'), (20, 20, '李四'), (30, 30, '王五');

步骤2:开启会话A,设置RC隔离级别,开启事务,执行第一次快照读

ini 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM test_user WHERE id=10;

执行结果:id=10,age=10,name=张三

步骤3:开启会话B,更新id=10的记录,提交事务

ini 复制代码
BEGIN;
UPDATE test_user SET age=11 WHERE id=10;
COMMIT;

步骤4:会话A执行第二次相同的快照读

ini 复制代码
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隔离级别,开启事务,执行第一次快照读

ini 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM test_user WHERE id=10;

执行结果:id=10,age=10,name=张三

步骤3:开启会话B,更新id=10的记录,提交事务

ini 复制代码
BEGIN;
UPDATE test_user SET age=11 WHERE id=10;
COMMIT;

步骤4:会话A执行第二次相同的快照读

ini 复制代码
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表初始数据

sql 复制代码
TRUNCATE TABLE test_user;
INSERT INTO `test_user` (`id`, `age`, `name`) VALUES (10, 10, '张三'), (20, 20, '李四'), (30, 30, '王五');

步骤2:开启会话A,设置RC隔离级别,开启事务,执行第一次当前读

sql 复制代码
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,插入一条符合范围的记录,提交事务

sql 复制代码
BEGIN;
INSERT INTO test_user (id, age, name) VALUES (25, 25, '赵六');
COMMIT;

步骤4:会话A执行第二次相同的当前读

sql 复制代码
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隔离级别,开启事务,执行第一次当前读

sql 复制代码
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,执行相同的插入操作,会被阻塞

sql 复制代码
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 复制代码
<?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:

ruby 复制代码
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 实体类

kotlin 复制代码
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接口

less 复制代码
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 服务层实现

ini 复制代码
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 控制层实现

less 复制代码
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 启动类

kotlin 复制代码
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标准更强的隔离性。

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

相关推荐
殷紫川2 小时前
击穿 MySQL InnoDB MVCC 底层:从 undo log、Read View 到隔离级别的全链路深度拆解
mysql
Full Stack Developme3 小时前
MySQL 触发器 存储过程 介绍
数据库·mysql
杨云龙UP3 小时前
MySQL慢查询日志暴涨导致磁盘告警:slow query log膨胀至397G的生产故障排查:清理、参数优化
linux·运维·服务器·数据库·mysql
Bat U3 小时前
MySQL数据库|视图+索引
数据库·mysql
想唱rap4 小时前
线程之条件变量和生产消费模型
java·服务器·开发语言·数据库·mysql·ubuntu
RInk7oBjo4 小时前
MySQL的编译安装
数据库·mysql·adb
java资料站4 小时前
MySQL 增量同步脚本
android·数据库·mysql
殷紫川4 小时前
InnoDB 索引性能天花板:聚簇 & 二级索引存储本质拆解,覆盖索引零回表优化全攻略
mysql
殷紫川4 小时前
MySQL IN 里塞 10000 个值?90% 开发者都踩过的坑,底层原理 + 全场景解决方案一次讲透
mysql