当前位置: 首页 > news >正文

击穿 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环境可直接执行

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

  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标准更强的隔离性。

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

http://www.cnnetsun.cn/news/1755770.html

相关文章:

  • Markmap:用Markdown语法一键生成动态思维导图
  • OpenClaw云端体验捷径:星图平台百川2-13B镜像+自动化框架一键部署
  • AI 生成的图片终于能写对字了:Wan 2.7 / Qwen Image 2.0 / Seedream 4.5 文字渲染实测
  • OpenClaw备份方案:Qwen3.5-9B自动化加密归档重要文件实践
  • 别再说电容是‘短路’了!手把手分析Pspice仿真中三极管极间电容对高频放大的影响
  • 穿越机飞控调参进阶:如何用FFT分析搞定高频振动问题
  • 2026年想轻松畅玩?三角洲俱乐部代肝服务,你不可错过的游戏助力!
  • 哔哩下载姬DownKyi:开源B站视频下载工具的全方位解析与实战指南
  • 新手必看:3步部署Yi-Coder-1.5B代码生成工具
  • 人到中年,哪有什么真正的轻松可言。
  • UR机械臂ROS2驱动选型指南:深入对比Ethernet RTDE与EtherCAT,你的项目该怎么选?
  • 深度解析:FakeLocation如何用应用级虚拟定位重塑Android隐私保护
  • EasyAnimateV5图生视频教程:如何用LoRA Alpha=0.55增强特定风格表现力
  • 云容笔谈·东方红颜与MATLAB联调:利用科学计算环境进行图像效果分析与优化
  • 黑丝空姐-造相Z-Turbo企业级数据库集成:MySQL存储与管理生成结果
  • 猫抓:革新性网页资源捕获工具的高效媒体下载解决方案
  • 零基础入门Chandra:图片转Markdown完整教程,保留排版超简单
  • 终极指南:如何使用 Deepin Boot Maker 快速制作 Linux 启动盘
  • 终极RimWorld MOD管理指南:用RimSort告别模组冲突烦恼
  • 掏空六个钱包挤进“富人区”,你到底被抽干了多少里子?
  • 城市基础设施健康状态检测数据集8599张VOC+YOLO
  • CYBER-VISION智能助盲系统部署指南:Dify平台保姆级教学
  • 抓住漫剧流量红利,JAVA 源码快速搭建专属短剧平台
  • 如何快速安装网盘直链下载助手:新手完整入门指南
  • 突破语言壁垒:FigmaCN让中文设计师效率提升300%
  • Zotero-GPT实战指南:AI驱动的文献智能管理深度解析
  • TPFanCtrl2:开源风扇控制工具助力ThinkPad用户破解散热困境
  • 零基础Git入门:Linux+Gitee实战指南
  • 第三次作业 f23016317 张书宁
  • S2-Pro Vue.js组件智能生成:根据需求描述快速产出前端UI代码