击穿 InnoDB 事务隔离级别:RC 与 RR 的底层实现、锁机制、MVCC 与幻读终极拆解
前言业务中90%的数据不一致、死锁、并发异常问题根源都在于对InnoDB事务隔离级别的底层实现理解不到位。本文从底层基石出发彻底拆解InnoDB事务隔离的核心原理重点对比RC读已提交与RR可重复读两大常用隔离级别的核心差异配合可复现的实例让你彻底搞懂底层逻辑不再踩坑。一、SQL标准隔离级别与读异常基础1.1 三大读异常定义脏读事务A读取了事务B未提交的修改数据若B回滚A读取的数据即为脏数据。不可重复读同一个事务内两次相同的主键查询返回了不同的行内容行数据被修改。幻读同一个事务内两次相同的范围查询第二次返回了第一次没有的行新增/删除行导致行数变化。1.2 SQL标准4个隔离级别隔离级别脏读不可重复读幻读读未提交READ UNCOMMITTED允许允许允许读已提交READ COMMITTEDRC禁止允许允许可重复读REPEATABLE READRR禁止禁止允许串行化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 UPDATEUPDATE/DELETE/INSERT会自动加X锁意向锁IS/IX表级锁用于快速判断表内是否存在行锁避免全表扫描判断锁冲突。加行S/X锁前必须先加对应的表级IS/IX锁意向锁之间互相兼容仅与表级S/X锁互斥。2.1.2 行锁算法核心InnoDB的行锁是加在索引上的无有效索引会退化为表锁核心行锁算法分为3种记录锁Record Lock仅锁住索引中的某一行记录仅针对存在的记录生效。间隙锁Gap Lock锁住索引记录之间的间隙不锁记录本身唯一作用是防止其他事务在间隙中插入数据解决幻读。间隙锁之间互相兼容仅与插入操作互斥。临键锁Next-Key LockInnoDB 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_id6字节最后一次修改该行的事务ID仅修改数据的事务会分配唯一递增的事务ID只读事务不分配trx_id为0。roll_pointer7字节回滚指针指向该行对应的undo log记录通过undo log构建数据的版本链。DB_ROW_ID6字节隐藏主键仅当表没有定义主键时生成用于构建聚簇索引。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即全局最大事务ID1。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT 测试用户表; INSERT INTO test_user (id, age, name) VALUES (10, 10, 张三), (20, 20, 李四), (30, 30, 王五);实例1RC级别下的行锁范围步骤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级别下仅对age10、age20的两条记录加记录锁不会锁住(10,20)的间隙所以会话B可以正常插入age15的记录无锁冲突。实例2RR级别下的行锁范围步骤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插入age15的记录会触发间隙锁冲突被阻塞直到会话A提交事务。3.1.2 锁释放时机与半一致性读差异半一致性读RC级别下UPDATE语句执行时若遇到已经加了X锁的记录InnoDB会先读取该记录的最新提交版本判断是否符合UPDATE的WHERE条件若不符合就跳过该记录不加锁若符合才会加锁等待。RR级别不支持半一致性读遇到加锁的记录直接加锁等待。实例3RC与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 age10;步骤3开启会话B设置RC隔离级别开启事务执行UPDATE语句可正常执行无阻塞SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN; UPDATE test_user SET name李四更新 WHERE age20; COMMIT;原理RC级别下会话B的UPDATE语句扫描到age10的记录时发现已经加了X锁通过半一致性读读取最新提交版本判断age10不符合WHERE条件直接跳过不加锁仅对age20的记录加锁所以无冲突。若将两个会话的隔离级别改为RR步骤3的UPDATE语句会被阻塞因为RR级别不支持半一致性读会话B扫描到age10的记录时不管是否符合条件都会加X锁等待直到会话A提交事务。3.2 MVCC实现的核心区别RC与RR的MVCC实现可见性判断规则完全一致核心差异在于Read View的生成时机这也是不可重复读问题的根源。隔离级别Read View生成时机RC事务中每次执行快照读普通SELECT时都会重新生成一个全新的Read ViewRR事务中第一次执行快照读普通SELECT时生成一个Read View整个事务生命周期内复用该Read View3.2.1 不可重复读的实例验证实例4RC级别下的不可重复读步骤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 id10;执行结果id10age10name张三步骤3开启会话B更新id10的记录提交事务BEGIN; UPDATE test_user SET age11 WHERE id10; COMMIT;步骤4会话A执行第二次相同的快照读SELECT * FROM test_user WHERE id10;执行结果id10age11name张三出现不可重复读。原理RC级别下会话A的两次SELECT都生成了新的Read View。第二次生成Read View时会话B的事务已经提交所以会话B修改的版本对会话A可见导致两次查询结果不一致。实例5RR级别下的可重复读保证步骤1初始化数据恢复test_user表初始数据 步骤2开启会话A设置RR隔离级别开启事务执行第一次快照读SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT * FROM test_user WHERE id10;执行结果id10age10name张三步骤3开启会话B更新id10的记录提交事务BEGIN; UPDATE test_user SET age11 WHERE id10; COMMIT;步骤4会话A执行第二次相同的快照读SELECT * FROM test_user WHERE id10;执行结果id10age10name张三实现了可重复读。原理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 幻读的实例验证实例6RC级别下当前读的幻读步骤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条记录id10、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条记录多了id25的行出现幻读。原理RC级别下会话A的当前读仅对age10、20、30的三条记录加记录锁不会锁住间隙所以会话B可以插入age25的记录导致会话A第二次当前读出现了新的行触发幻读。实例7RR级别下对幻读的彻底解决步骤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条记录id10、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插入age25的记录会触发间隙锁冲突被阻塞直到会话A提交事务彻底杜绝了幻读。四、核心流程图与架构图4.1 Read View可见性判断流程图4.2 RC与RR的Read View生成时机对比图4.3 临键锁区间示意图五、Java实战基于MyBatis-Plus验证隔离级别差异5.1 项目依赖配置pom.xml核心依赖?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent groupIdcom.jam.demo/groupId artifactIdinnodb-isolation-demo/artifactId version0.0.1-SNAPSHOT/version nameinnodb-isolation-demo/name properties java.version17/java.version mybatis-plus.version3.5.7/mybatis-plus.version fastjson2.version2.0.52/fastjson2.version guava.version33.1.0-jre/guava.version lombok.version1.18.32/lombok.version springdoc.version2.5.0/springdoc.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version${springdoc.version}/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version${lombok.version}/version scopeprovided/scope /dependency dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version${fastjson2.version}/version /dependency dependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version${guava.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin /plugins /build /project5.2 配置文件application.ymlspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/test_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse 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: true5.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 BaseMapperTestUser { /** * 带排他锁的范围查询 * param minAge 最小年龄 * param maxAge 最大年龄 * return 符合条件的用户列表 */ Select(SELECT * FROM test_user WHERE age BETWEEN #{minAge} AND #{maxAge} FOR UPDATE) ListTestUser 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 TransactionCallbackString() { 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 TransactionCallbackString() { 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 TransactionCallbackString() { Override public String doInTransaction(TransactionStatus status) { log.info(RC事务开启第一次范围查询年龄:{}-{}, minAge, maxAge); ListTestUser 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); ListTestUser 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标准更强的隔离性。所有的并发问题本质上都是对隔离级别底层实现的理解不到位。只有彻底搞懂底层原理才能写出高并发、高一致性的业务代码彻底杜绝数据不一致、死锁等线上问题。