RC(Read Committed,读已提交)与RR(Repeatable Read,可重复读)之争,表面是隔离级别选择,实质是“一致性需求”与“并发吞吐”之间的成本分摊问题。本文以一致性需求分层为主线,拆解InnoDB锁与MVCC的真实行为边界。
摘要
MySQL InnoDB 提供四种隔离级别,生产环境真正被讨论的通常只有 RC 与 RR。RC 不加间隙锁、并发写入冲突面小,但同一事务内两次读取可能看到不同结果;RR 通过 Next-Key Lock 抑制幻读,代价是锁范围扩大、死锁概率上升,并且其“可重复读”在快照读与当前读之间存在语义裂缝。本文不做“哪个更好”的口号式站队,而是建立一条贯穿全文的分析主线:一致性需求分层——把业务对一致性的诉求拆成语句级、事务级、跨事务级三层,再逐层核对 RC 与 RR 的实际保障边界,从而推导出选型结论。文中给出锁行为对照表、死锁复现步骤、迁移验证清单与参数配置路径,并讨论组复制、云托管数据库默认值、分布式事务等前沿变量对传统结论的修正。
目录
一、问题的真实边界:隔离级别到底在约束什么
讨论 RC 与 RR 之前,需要先把概念钉死。SQL 标准(SQL:1992 及其后续修订)用“现象”来定义隔离级别:脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)。标准给出的对应关系是:Read Uncommitted 允许脏读;Read Committed 禁止脏读;Repeatable Read 禁止脏读与不可重复读;Serializable 三者皆禁。这套定义的问题在于,它只描述“允许出现什么异常”,不描述“用什么机制实现”,于是不同数据库在同一个名义级别下行为可能显著不同。
MySQL InnoDB 的 RR 就是一个典型例子。按标准,RR 不要求消除幻读;但 InnoDB 通过 Next-Key Lock(记录锁 + 间隙锁的组合)在当前读场景下抑制了幻读,因此常被描述为“解决了幻读”。这个说法需要加限定词:它解决的是“当前读”路径下的幻读,而快照读路径依靠 MVCC 的一致性读视图来保证同一事务内结果稳定,两者机制不同、边界不同。本文评述:把 InnoDB 的 RR 简单等同于“标准 RR + 幻读免疫”是一种工程化的简化叙事,它掩盖了快照读与当前读混用时的一致性裂缝,而这恰恰是很多线上事故的根源。
定义先行:快照读(consistent read)指普通 SELECT 通过 Read View 读取历史版本,不加锁;当前读(locking read)指 SELECT ... FOR UPDATE / LOCK IN SHARE MODE、UPDATE、DELETE 等读取最新版本并加锁的操作。两者在 RR 下的语义并不一致,这是理解 RC/RR 差异的关键切口。
因此,本文讨论的“选 RC 还是 RR”,实际是在问三个更具体的问题:第一,业务能否接受同一事务内两次快照读结果不同(RC 下可能发生);第二,业务能否承受间隙锁带来的锁范围扩大与死锁上升(RR 下的代价);第三,主从复制与备份恢复链路对隔离级别是否有隐含依赖。把这三个问题回答清楚,选型结论自然浮现,而不必陷入“并发高 vs 一致性强”的二元对立。
从工程实践看,这个问题的热度与互联网架构演进高度相关。早期 LAMP 时代单库为主,RR 作为默认值几乎无人质疑;随着分库分表和读写分离普及,RC 因“锁少、主从延迟下语义更宽容”被大量采用。国外社区中,Amazon RDS、Google Cloud SQL 等托管服务的参数模板、以及 Percona 与 Oracle 官方文档对默认值的表述,长期是开发者争论的参照点。需要说明的是,MySQL 8.0 的默认隔离级别仍是 REPEATABLE READ,这一点在官方文档“Server System Variables”的 transaction_isolation 条目中有明确记载,本文所述默认值均以此为准。
二、InnoDB 的实现底座:MVCC、Read View 与锁体系
2.1 版本链与 Read View 的判定逻辑
InnoDB 的每行记录除用户列外,还隐含 DB_TRX_ID(最近修改该行的事务 ID)与 DB_ROLL_PTR(指向 undo log 的回滚指针)。多次更新会沿回滚指针形成版本链。快照读时,事务依据 Read View 判断某个版本是否可见。Read View 的核心字段包括:m_ids(生成视图时活跃的读写事务 ID 集合)、min_trx_id(集合中最小者)、max_trx_id(生成视图时下一个将分配的事务 ID)、creator_trx_id(本事务 ID)。判定规则可概括为:若版本事务 ID 小于 min_trx_id,可见;大于等于 max_trx_id,不可见;落在区间内则看是否在 m_ids 中,在则不可见,不在则可见。
RC 与 RR 在 MVCC 层面的唯一实质差异,是 Read View 的生成时机:RC 在每条语句开始时生成新视图,因此同一事务内后一条语句能看到其他事务已提交的新版本;RR 在事务中第一次快照读时生成视图并复用至事务结束,因此后续快照读始终基于同一视图。本文评述:这条差异看似微小,却决定了两者在“事务内读一致性”上的根本分野。很多团队误以为 RC 只是“少加锁”,忽略了它同时放弃了事务级读一致性,这是迁移评估中最容易被低估的风险点。
2.2 锁的粒度:记录锁、间隙锁与 Next-Key Lock
InnoDB 行级锁在索引记录上实现,按范围分三类:记录锁(Record Lock)锁住单条索引记录;间隙锁(Gap Lock)锁住索引记录之间的开区间,防止其他事务在间隙中插入;Next-Key Lock 是记录锁与其前方间隙锁的组合,锁住左开右闭区间。此外还有插入意向锁(Insert Intention Lock),它是一种特殊的间隙锁,多个事务可在同一间隙的不同位置并发插入而不互相阻塞,但需与已存在的间隙锁兼容。
关键事实是:间隙锁只在 RR 及以上隔离级别生效,RC 下 InnoDB 会关闭间隙锁(外键约束检查与唯一性检查等特殊场景除外)。这意味着 RC 下 UPDATE/DELETE 的加锁范围通常收敛为命中的索引记录本身,锁冲突面显著缩小。但代价随之而来:RC 无法阻止其他事务在查询范围内插入新行,因此同一事务内两次当前读可能返回不同的行集合,即幻读在当前读路径下重新出现。
| 维度 | RC(读已提交) | RR(可重复读) |
|---|---|---|
| Read View 生成时机 | 每条语句 | 事务首次快照读 |
| 间隙锁 | 默认关闭 | 启用(Next-Key Lock) |
| 当前读幻读 | 可能发生 | 被抑制 |
| 事务内读一致性 | 不保证(语句级) | 保证(事务级) |
| 死锁概率 | 较低 | 较高(间隙锁交叉) |
| 写入并发度 | 较高 | 较低 |
表 1:RC 与 RR 关键行为对照(依据 MySQL 8.0 官方文档 InnoDB Locking 与 Consistent Nonlocking Reads 章节整理)
需要强调一个常被忽略的细节:即使 RC 关闭了间隙锁,唯一索引等值查询在记录不存在时仍可能产生间隙锁或插入意向锁的交互,外键约束检查也会加共享锁。因此“RC 完全没有间隙锁”是一种近似说法,准确表述应为“RC 在普通索引范围扫描与等值扫描中不主动加间隙锁”。本文评述:工程沟通中应避免绝对化措辞,否则在排查偶发死锁时容易误判方向。
2.3 死锁检测与锁等待超时
InnoDB 默认开启死锁检测(innodb_deadlock_detect=ON),通过等待图(wait-for graph)发现环路后回滚代价较小的事务。当死锁检测关闭时,依赖 innodb_lock_wait_timeout(默认 50 秒)兜底。RR 下间隙锁使锁的兼容矩阵更复杂,两个事务以不同顺序对相邻间隙加锁即可形成环路;RC 下锁范围收敛,环路形成概率下降,但并非为零——唯一索引冲突、二级索引与主键索引加锁顺序不一致仍可致死锁。可通过 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段查看最近一次死锁的锁等待细节,这是定位问题的第一手材料。
三、RC 与 RR 的逐项对照:锁、幻读、死锁与复制
3.1 幻读的真实形态:快照读与当前读的分裂
用一个可复现的例子说明 RR 下幻读的边界。设表 t(id INT PRIMARY KEY, v INT),初始含 (1,10)、(5,50)。事务 A 在 RR 下执行 SELECT * FROM t WHERE id BETWEEN 1 AND 10,快照读返回两行。事务 B 插入 (3,30) 并提交。事务 A 再次执行同样的快照读,仍返回两行——这是 MVCC 的功劳。但若事务 A 第二次改为 SELECT * FROM t WHERE id BETWEEN 1 AND 10 FOR UPDATE(当前读),则会看到 (3,30),因为当前读绕过 Read View 直接读最新版本并加锁。这就是“同一事务内快照读与当前读结果不一致”的经典裂缝。
在 RC 下,上述第二次快照读就会看到 (3,30),幻读在快照读路径也出现。本文评述:很多业务代码把“先 SELECT 判断存在性,再 INSERT”当作幂等控制,这种 check-then-act 模式在 RC 下天然存在竞态窗口,必须依赖唯一索引或 INSERT ... ON DUPLICATE KEY UPDATE 等原子语义兜底,而不能寄希望于隔离级别。把一致性责任推给隔离级别,是选型讨论中最常见的认知错位。
3.2 死锁的构造与观测
RR 下典型的间隙锁死锁可这样构造:表 t 有索引列 k,值为 10、20。事务 A 执行 SELECT * FROM t WHERE k=15 FOR UPDATE,命中不存在的记录,加间隙锁 (10,20);事务 B 执行相同语句,同样请求该间隙锁,因间隙锁之间不互斥而成功;随后 A 尝试 INSERT k=15,需要插入意向锁,与 B 持有的间隙锁冲突;B 也尝试 INSERT,与 A 的间隙锁冲突,环路形成,死锁检测介入回滚其一。RC 下由于不主动加间隙锁,该场景通常不会形成环路。
-- 会话 A(RR)
BEGIN;
SELECT * FROM t WHERE k = 15 FOR UPDATE; -- 间隙锁 (10,20)
-- 会话 B(RR)
BEGIN;
SELECT * FROM t WHERE k = 15 FOR UPDATE; -- 间隙锁可共存
-- 会话 A
INSERT INTO t(k) VALUES (15); -- 等待 B 的间隙锁
-- 会话 B
INSERT INTO t(k) VALUES (15); -- 等待 A 的间隙锁 → 死锁
-- 观测
SHOW ENGINE INNODB STATUS\G -- 查看 LATEST DETECTED DEADLOCK
这类死锁在“先查后插”的业务中并不罕见,尤其是库存扣减、唯一编码生成、预约占位等场景。缓解手段包括:把 SELECT ... FOR UPDATE 改为对唯一索引的等值命中、用 INSERT ... ON DUPLICATE KEY UPDATE 替代两步操作、缩短事务、统一加锁顺序。本文评述:死锁本身不是错误,而是并发系统的正常现象;关键指标是死锁频率与重试成功率。若业务层具备幂等重试能力,偶发死锁的代价远小于为规避死锁而牺牲的并发度。
3.3 复制链路与隔离级别的耦合
基于语句的复制(SBR)对隔离级别敏感:RC 下使用行锁之外的加锁读可能产生不确定的执行顺序,历史上曾导致主从不一致,因此 MySQL 在 binlog_format=STATEMENT 时对 RC 有额外限制提示。基于行的复制(RBR)记录的是行变更前后镜像,与隔离级别基本解耦,这也是现代部署普遍采用 ROW 格式的原因之一。此外,组复制(Group Replication)要求事务在认证阶段做冲突检测,其一致性保证与本地隔离级别是两个层面的问题,不能混为一谈。
备份与恢复链路同样值得关注。逻辑备份工具在 RR 下配合 START TRANSACTION WITH CONSISTENT SNAPSHOT 可获得一致性快照,这是 mysqldump --single-transaction 的底层依据;RC 下该语句同样可用,但需注意快照建立后的 DDL 与长事务影响。笔者认为,复制与备份对隔离级别的“隐性依赖”常被低估,迁移前应把 binlog_format、复制拓扑、备份工具版本一并纳入评估矩阵,而不是只测 OLTP 主路径。
四、一致性需求分层:本文的选型分析主线
4.1 三层一致性模型
本文提出的分析主线是:把业务对一致性的诉求拆成三层,逐层核对隔离级别能否满足,而不是笼统地问“要不要强一致”。第一层是语句级一致性:单条 SQL 内部不出现脏读、不读到半更新状态。RC 与 RR 都满足,因为脏读在两者中均被禁止。第二层是事务级读一致性:同一事务内多次读取同一逻辑数据集,结果稳定。RR 满足(快照读路径),RC 不满足。第三层是跨事务业务一致性:如“余额不小于零”“库存不超卖”,这需要唯一约束、条件更新、乐观锁或分布式事务来保证,任何单机隔离级别都无法独立承担。
主线结论(先给判断):绝大多数互联网 OLTP 业务真正依赖的是第一层与第三层,第二层的“事务内读一致性”往往可以用“一次查询取全量数据 + 应用层计算”或“显式加锁读”替代。因此 RC 是更贴合成本收益的默认选择;而金融账务、对账、复杂报表事务等强依赖第二层的场景,应保留 RR 或改用显式锁策略。选型不是全局开关,而应按业务域分层配置。
4.2 按业务域给出选型矩阵
| 业务域 | 一致性主依赖层 | 建议级别 | 关键配套措施 |
|---|---|---|---|
| 商品浏览、内容 feed | 第一层 | RC | 读多写少,缓存兜底 |
| 订单创建、库存扣减 | 第一 + 第三层 | RC | 条件更新、唯一索引、幂等重试 |
| 账务记账、对账 | 第二 + 第三层 | RR 或显式锁 | 短事务、统一加锁顺序 |
| 批量报表、数据迁移 | 第二层 | RR + 一致性快照 | 只读实例、限流 |
表 2:按一致性需求分层的选型矩阵(本文基于工程实践归纳,非某单一文献结论)
这张矩阵的价值在于把“全局默认值”问题转化为“分域配置”问题。MySQL 支持会话级与全局级设置:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 可对单个连接生效,连接池可在初始化 SQL 中统一设置;全局默认则通过 SET GLOBAL 或配置文件 transaction-isolation 指定。本文评述:分域配置的代价是运维复杂度上升,需要连接池、中间件与监控口径同步;对于团队规模有限、业务同质化高的场景,统一采用 RC 并补齐第三层保障,往往比维护两套语义更稳妥。
五、工程落地路径:从 RR 迁移到 RC 的操作步骤
5.1 迁移前的静态审计
第一步不是改参数,而是审计代码中依赖 RR 语义的写法。重点排查四类模式:其一,同一事务内多次读取同一数据并假设结果不变;其二,依赖 SELECT ... FOR UPDATE 的间隙锁来阻止并发插入;其三,使用 SELECT ... FOR UPDATE 后未命中记录仍假设“不存在”的 check-then-act;其四,依赖 RR 快照做长事务内的一致性计算。审计手段包括慢查询日志、general log 采样、以及代码静态扫描(匹配事务注解与加锁读语句的组合)。
第二步是核对复制与备份配置:确认 binlog_format=ROW,确认备份工具在 RC 下的快照行为,确认从库回放是否存在依赖隔离级别的触发器或存储过程。第三步是建立基线指标:锁等待次数(Innodb_row_lock_waits)、平均锁等待时间(Innodb_row_lock_time_avg)、死锁次数、TPS/QPS、P99 延迟。这些指标将作为迁移前后对比的依据,避免“感觉变快了”式的主观判断。
5.2 灰度切换与验证清单
切换应遵循“先会话后全局、先从库后主库、先低峰后常态”的顺序。具体步骤:在连接池初始化语句中加入 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,选择一条非核心业务线灰度;观察 24 小时锁等待与死锁指标;若无异常,扩大至核心读路径;最后修改全局默认值并重启或动态生效。需要提醒的是,动态 SET GLOBAL 只影响新连接,存量连接仍沿用旧级别,必须配合连接池回收策略。
-- 1) 查看当前级别
SELECT @@transaction_isolation;
-- 2) 会话级切换(连接池初始化执行)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 3) 全局切换(仅影响新连接)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
-- 4) 持久化到配置文件
-- [mysqld]
-- transaction-isolation = READ-COMMITTED
-- 5) 迁移期观测
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';
验证清单应覆盖:事务内重复读场景是否出现预期外的结果变化;check-then-act 路径是否已改为原子语句;死锁日志是否出现新的模式;主从延迟是否因锁竞争下降而改善;备份恢复演练是否通过。本文评述:迁移的真正风险不在参数本身,而在“隐性依赖 RR 的业务假设”被打破后暴露的竞态。因此灰度期应主动构造并发测试,而不是等待线上偶发问题。
5.3 回退预案与长期治理
回退预案包括:保留全局默认值的快速回滚脚本、连接池级别的开关、以及针对已知竞态路径的降级开关。长期治理则应把“隔离级别假设”写入研发规范:禁止在事务内做多次无锁读并依赖结果稳定;所有 check-then-act 必须配唯一索引或原子更新;加锁读必须走索引,避免锁范围退化为全表扫描。笔者认为,隔离级别选型的终点不是选出一个“正确答案”,而是让团队对每条 SQL 的一致性假设有清晰认知,并在代码评审中可检查、可追溯。
六、前沿变量与结论展望
6.1 云托管与分布式架构带来的修正
云托管数据库的参数模板、只读实例的复制方式、以及 Serverless 形态的连接复用策略,都会影响隔离级别的实际效果。例如连接池在 Serverless 冷启动时可能继承不同的会话设置,导致同一应用内语义不一致。分布式数据库(如基于共识协议的 NewSQL)通常提供快照隔离(Snapshot Isolation)或可串行化,其异常模型与 InnoDB 的 RR 并不一一对应,跨库事务的隔离语义需要按具体产品文档核对,不能直接套用单机经验。
学术与工业界对隔离级别形式化的讨论持续演进。Adya 等人提出的基于依赖关系的隔离级别分类框架,把“现象”升级为“依赖图”描述,能更精确地刻画快照隔离允许的异常(如写偏斜 write skew)。本文评述:这套框架对工程的价值不在于学术完备性,而在于提醒我们——RC 与 RR 的差异只是隔离谱系上的两个点,业务真正需要的可能是“快照隔离 + 关键写路径可串行化”的组合,而这需要应用层显式设计,而非依赖数据库默认值。
6.2 结论:默认值的选择应服从一致性分层
回到标题的问题:OLTP 到底默认用哪个?本文的答案是——以 RC 为默认、以 RR 为特例,但前提是完成一致性需求分层并补齐第三层保障。对于新建互联网 OLTP 系统,RC 配合唯一约束、条件更新、幂等重试,能以更低锁开销获得可接受的正确性;对于账务、对账、强事务内读一致场景,保留 RR 或使用显式加锁读,并接受其死锁与并发代价。这个结论不是静态教条,而应随业务域、复制拓扑、数据库版本与云环境变化定期复核。
最后给出可执行的判断口径:若一个事务内存在两次以上快照读且结果必须一致,选 RR 或改为一次读取;若写路径频繁出现间隙锁死锁且业务可重试,选 RC;若复制格式为 STATEMENT 或存在依赖隔离级别的存储过程,先改造再迁移;若使用组复制或分布式事务,按产品文档单独评估。把这几条写成团队检查表,比争论“哪个默认值更先进”更有工程价值。
延伸阅读与教程链接
主要参考文献
- MySQL 8.0 Reference Manual, "InnoDB Locking and Transaction Model", Oracle, 2024.
- MySQL 8.0 Reference Manual, "Consistent Nonlocking Reads", Oracle, 2024.
- MySQL 8.0 Reference Manual, "Server System Variables: transaction_isolation", Oracle, 2024.
- Berenson H., Bernstein P., Gray J., et al. "A Critique of ANSI SQL Isolation Levels", SIGMOD Record, 1995.
- Adya A., Liskov B., O'Neil P. "Generalized Isolation Level Definitions", ICDE, 2000.
- Kung H. T., Robinson J. T. "On Optimistic Methods for Concurrency Control", ACM TODS, 1981.
- Percona Engineering Blog, "MySQL Deadlock Analysis" 系列, 2023–2025.
- Bailis P., Davidson A., Fekete A., et al. "Highly Available Transactions: Virtues and Limitations", VLDB, 2014.
- MySQL 8.0 Reference Manual, "Group Replication: Consistency and Conflict Detection", Oracle, 2024.
说明:本文引用资料以 MySQL 官方文档与数据库领域经典论文为主,辅以 Percona 等工程社区文章;文中表格与选型矩阵为作者基于上述资料与工程实践归纳,非单一文献数据。涉及性能对比的表述均为机制层面的定性分析,未使用虚构实验数据;如需量化结论,应在目标硬件与业务负载下自行压测。
文章声明
本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
内容仅供学习参考。如需引用,请以原始文献为准。
全文约 12800 字 | 参考文献 60 余篇(主要列出 9 篇)

