【MySQL百日打怪升级第18天】事务隔离级别详解:RC、RR、Serializable 到底怎么选?

简介: 本系列第18天详解MySQL事务隔离级别:RC(读已提交)、RR(可重复读,默认)、Serializable。厘清脏读、不可重复读、幻读三大问题,剖析InnoDB下RR如何借MVCC+Gap Lock基本防幻读,并给出RC与RR选型建议及真实避坑指南。

【第18天】每天一个MySQL知识点,百日打怪升级

事务隔离级别详解:RC、RR、Serializable 到底怎么选?


大家好,我是一名拥有10年以上经验的DBA老兵,没有那多。

做这个系列,源于一个朴素的愿望:把踩过的坑、总结的经验系统化输出,希望能帮到刚入行或想进阶的兄弟们。

让我们开始今天的第18天内容。


背景引入

💡 同一个 SELECT 查两次,结果不一样——这是 bug 吗?

有个开发同事跑过来找我,说发现了一个"bug"。

他的代码逻辑是这样:先查一下订单状态,如果是"待支付"就执行后续逻辑。但有时候第一次查是"待支付",第二次查就变成了"已支付"——中间隔了不到 100 毫秒。

他问我:"是不是 MySQL 缓存有问题?"

我说:"不是缓存问题,是隔离级别的问题。"

问了一下——他们用的 READ COMMITTED(RC)。RC 下每条 SELECT 读的是最新已提交的数据,所以另一个事务 COMMIT 了支付之后,他的第二次 SELECT 自然就读到了新状态。

这就是不可重复读:同一个事务里两次 SELECT,结果不一样。

这其实不是 bug,而是 READ COMMITTED 隔离级别的"特性"。如果换成 REPEATABLE READ(RR,MySQL 默认),就不会有这个问题——事务开始时拍一次快照,后面所有普通 SELECT 都读这个快照,哪怕别人提交了也看不见。

这就是隔离级别要解决的问题:多个事务同时跑,你允许它们互相看到多少?

今天的目标:搞懂四个隔离级别,以及它们在 MySQL 里的实际行为。


核心概念

隔离级别解决的问题

多个事务并发执行,会出现三个经典问题:

问题 现象 通俗解释
脏读 读到别的事务还没提交的数据 A 改了数据没提交,B 读到了;A 回滚了,B 拿着一个不存在的脏数据
不可重复读 同一条 SELECT 两次执行,读到不同的已提交数据 第一次读余额 100,别人转进来 50 并提交了,第二次读余额变成 150
幻读 同一个条件查两次,行数不一样 第一次查 WHERE status='pending' 返回 5 行,别人插入了一条并提交了,第二次返回 6 行

三个问题严重程度:脏读 > 不可重复读 > 幻读。


四个隔离级别怎么对应

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED(RC) 避免 可能 可能
REPEATABLE READ(RR) 避免 避免 InnoDB 基本避免
SERIALIZABLE 避免 避免 避免

这张表面试必考,背下来。

注意一个关键差异:SQL 标准说 RR 不防幻读,但 InnoDB 的 RR 大多数情况下防幻读。 因为 InnoDB 在 RR 下用了 Gap Lock(间隙锁,锁住索引记录之间的空隙) + MVCC(多版本并发控制,通过 Undo Log 实现多版本数据共存),能阻止幻行插入。

① 大部分场景下防住了,但有一个边缘情况:DML 语句(UPDATE/DELETE)不遵循快照读,可能看到其他事务刚提交的行,导致"快照读和 DML 看到的数据不一致"。详见后面实战案例。


MySQL 里各隔离级别的实现差异

特性 RC RR(默认) Serializable
快照读时机 每条语句执行时重新拍快照 事务开始时拍一次快照 没有快照读,全部当前读(locking read)
Gap Lock 关闭(只锁行) 开启 开启(更严)
性能 中等
适用场景 互联网高并发业务 默认选择,通用 强一致性场景

RC 下:每条 SELECT 都是独立的快照,能看到其他事务已提交的修改。所以同一个事务里两次 SELECT 可能结果不同(不可重复读)。

RR 下:第一条 SELECT 拍下快照,之后整个事务里所有普通 SELECT 都读这个快照。所以两次 SELECT 结果永远一致——代价就是事务开始后看不到其他事务新提交的数据。

Serializable 下:所有 SELECT 自动变成 SELECT ... FOR SHARE(共享锁),读也要锁行,并发基本归零。线上几乎不用,除非你做的业务一丁点并发都不能忍。


一个经典面试题:RC 和 RR 到底怎么选?

这个借用一个前同事的原话,我觉得说得特别好——"如果你要确保事务内多次读结果一致,选 RR;如果你更在意并发性能,而且能接受读到最新的已提交数据,选 RC。"

具体来说:

  • 选 RR:报表统计(同一个事务内多次 SUM/COUNT 要一致)、需要 Gap Lock 防幻读的场景
  • 选 RC:大部分互联网业务(订单、用户、商品),并发高,能接受不可重复读但必须避免脏读

很多互联网公司甚至默认就是 RC,配合 binlog_format=ROW,并发好不少。


实战案例

场景一:事务里两次 SELECT 结果不同——不可重复读

-- 事务 A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE id = 1;  -- 结果: 100
-- 此时事务 B 执行了 UPDATE account SET balance = 200 WHERE id = 1; COMMIT;
SELECT balance FROM account WHERE id = 1;  -- 结果: 200(不一样了!)
COMMIT;

这就是不可重复读。同一个事务里,两次查同一个 id,结果变了。在 RR 下不会发生:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM account WHERE id = 1;  -- 结果: 100
-- 事务 B 提交了 UPDATE
SELECT balance FROM account WHERE id = 1;  -- 结果: 100(跟第一次一样,读的是快照)
COMMIT;

场景二:幻读——InnoDB 的 RR 大部分情况下防得住,但不是绝对的

-- 事务 A(RR 级别)
BEGIN;
SELECT * FROM orders WHERE status = 'pending';  -- 返回 5 行
-- 此时事务 B INSERT 了一条 status='pending' 的订单并提交
SELECT * FROM orders WHERE status = 'pending';  -- 还是 5 行(MVCC 快照读,看不到新插入的)

但如果 A 在第二次 SELECT 之前执行了当前读(FOR UPDATE):

SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;  -- Gap Lock 阻止了 B 的插入,等 A 提交后 B 才能 INSERT

因为 FOR UPDATE 走的是当前读,而且 Gap Lock 在 status 的索引间隙上加了锁,事务 B 的 INSERT 会被阻塞,直到 A 提交。

但有一个容易被忽略的边缘情况:快照读和 DML 看到的数据不一致。

MySQL 官方文档明确记录了这个问题:在 RR 下,快照(Snapshot)只适用于 SELECT 语句,不适用于 DML 语句(UPDATE/DELETE)的读取部分。 当你的事务执行 DML 时,InnoDB 会走当前读(locking read)来读取数据,而不是事务开始时的那个快照。

具体表现:

-- 事务 A(RR)
BEGIN;
SELECT * FROM orders WHERE status = 'pending';  -- 返回 0 行(没有待支付的订单)
-- 此时事务 B INSERT 了一条 status='pending' 的订单并提交
-- (因为没有加锁,B 的 INSERT 不会被阻塞)

DELETE FROM orders WHERE status = 'pending';   -- 居然删掉了 B 刚插入的那行!
-- 为什么?因为 DELETE 的读部分走的是当前读,看得见 B 提交的数据

SELECT * FROM orders WHERE status = 'pending';  -- 依然返回 0 行(快照读还是旧的)
-- 但这并不能查询到刚才删除的行——因为那些行已经被当前事务删除了,而新的快照读看不到其他事务的数据

官方文档的原话(Consistent Nonlocking Reads):

The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them.

翻译一下:快照只对 SELECT 有效,不一定对 DML 有效。你用 SELECT 查不到的行,DELETE 可能删得掉。

这种现象虽然不是严格意义上的"幻读"(因为两次 SELECT 结果仍然一致),但本质上属于同一类问题——快照读和当前读之间的数据裂缝。

用人话说就是:你眼睛看到的数据(SELECT)和你动手操作的数据(DELETE/UPDATE)可能不是同一套——因为你眼睛看的是旧地图,手摸到的却是新地形。

如果你需要彻底杜绝这个问题,唯一的办法是 Serializable,或者在 RR 下给所有查询加 FOR UPDATE(但这相当于放弃了快照读的优势)。


避坑指南

⚠️ 真实踩过的坑:

  1. RR 下事务内的 UPDATE 会影响后续 SELECT 的结果

    • RR 的快照读是在事务内第一个 SELECT 时建立,但是当前读(UPDATE/DELETE/FOR UPDATE)会读取最新的已提交数据,并在此基础上进行操作
    • 之后事务的快照读会看到自己修改后的结果,看起来像是"快照被更新了"
    • 坑:事务里先 SELECT 看到旧数据,然后 UPDATE 了同一行,再 SELECT——结果变了。很多人以为 RR 是"从头到尾读一样",实际上当前读和快照读用的是两套数据
    • 这个面试也常问,记一下
  2. RC 下 binlog 必须用 ROW 格式

    • MySQL 官方文档有一句:binlog_format=STATEMENT 下如果用了 RC,会有警告
    • 因为 STATEMENT 格式记录的 SQL 在从库回放时可能产生不同的结果(不可重复读导致的行差异)
    • 最佳实践:RC + ROW 是一对
  3. Serializable 几乎不用,但面试爱问

    • 线上很少见,因为性能太差
    • 但如果你的业务要求"所有读都是当前读"(比如金融对账),可以考虑
    • 更多人是用 SELECT ... FOR UPDATE 在 RR 下实现类似效果
  4. SET TRANSACTIONSET SESSION 的区别

    • SET SESSION TRANSACTION ISOLATION LEVEL ... 只影响当前会话
    • SET GLOBAL TRANSACTION ISOLATION LEVEL ... 影响所有新会话
    • 改了全局配置后,已有连接不会生效——要重连
    • 线上改隔离级别记得分批重启连接,不要一把全踢

思考题

🤔 互动时间:

  1. RR 下,事务 A 先 SELECT 了一行,然后事务 B 修改并提交了这行,事务 A 再 UPDATE 这行——UPDATE 会成功吗?UPDATE 之后 A 再 SELECT 会看到什么?
    提示:假设事务 B 已经提交。RR 下当前读会读到最新版本的数据,之后事务的快照读会看到自己修改后的结果。想想 UPDATE 前后数据的变化。

  2. 为什么 MySQL 官方推荐 RC + ROW 格式 binlog?为什么不推荐 RR + STATEMENT?

  3. 一张表,没有任何并发写,只有纯查询——这时候用哪个隔离级别有区别吗?
    提示:想想 MVCC 快照的时机。


总结

🎯 面试考点

  • 四个隔离级别的演进关系:
RU ──────> RC ──────> RR ──────> Serializable
弱隔离/高并发              强隔离/低并发
  • 三个问题:脏读(未提交的数据)、不可重复读(已提交的数据变化)、幻读(行数变化)
  • InnoDB 的 RR 大部分场景防幻读,但 DML 语句可能"看到"快照之外的数据(快照只对 SELECT 有效)
  • RC vs RR:RC 每次 SELECT 新快照,适合高并发;RR 事务开始拍一次快照,适合一致性要求高的场景
  • RC 必须配合 ROW 格式 binlog
  • Serializable 线上慎用,面试常考

今天就试一下:查一下你数据库的 transaction_isolation 参数:

SHOW VARIABLES LIKE 'transaction_isolation';

看看你的业务跑在哪个隔离级别上,想一想——如果你的业务是 RR,改成 RC 会出问题吗?反过来呢?


下期预告:脏读、不可重复读、幻读 —— 三个现象一次讲透!

全本合集《每天一个MySQL知识点,百日打怪升级》


有问题欢迎评论区交流,明天见!

相关文章
|
30天前
|
边缘计算 安全 网络协议
AIWCLOUD:免备案高防CDN,抗投诉,大陆节点免备,在跨国企业混合办公场景下
后疫情时代,跨国企业混合办公面临跨境延迟、未备案域名访问受限及VPN安全隐患等痛点。本文介绍一种“免备案CDN”架构:融合边缘零信任接入(ZTNA)、动态端口敲门、SAP/RDP协议优化、HTTP/3加速、域名分片合规回源与实时数据脱敏,构建安全、合规、高性能的全球数字走廊。(239字)
201 4
|
消息中间件 存储 监控
RabbitMQ:分布式系统中的高效消息队列
RabbitMQ:分布式系统中的高效消息队列
|
30天前
|
缓存 监控 安全
别再让Docker占满你的硬盘!一篇搞定docker system所有命令
本指南详解 `docker system` 命令组,助你精准诊断与优雅清理 Docker 占用空间:`df` 查磁盘、`prune` 清资源、`info` 看配置、`events` 监事件。覆盖安全清理策略、自动化脚本与环境最佳实践,告别“磁盘爆满”焦虑。(239字)
215 2
别再让Docker占满你的硬盘!一篇搞定docker system所有命令
|
30天前
|
机器学习/深度学习 人工智能 自然语言处理
Agent = Model + Harness:模型决定上限Harness 决定下限
Claude Code、Cursor等并非聊天界面,而是AI编码的“操作系统”——Harness。它决定模型能否稳定执行规则、调用工具、管理上下文与权限。模型定上限,Harness定下限。差异常源于Harness配置,而非模型本身。
379 5
|
30天前
|
存储 供应链 安全
Gemini 多模态能力在企业云架构中的落地方式
企业落地 Gemini 多模态 API,需要把对象存储、异步任务、模型网关、审核流、日志审计和成本统计一起设计。本文给出一套云上架构拆解。
119 3
|
30天前
|
Rust 监控 安全
Kimsuky 组织基于 PebbleDash 与 AppleSeed 的攻击战术演进与技术分析
本文深度剖析朝鲜APT组织Kimsuky(APT43)2025–2026年最新攻击活动,聚焦其PebbleDash(含Rust编写的HelloDoor、httpMalice)与AppleSeed(含HappyDoor)双恶意软件集群,揭示鱼叉钓鱼、多格式下载器、内存加载、VSCode/DWAgent隧道劫持、LLM辅助编码等新型战术,为关键基础设施防御提供实战参考。(239字)
212 2
|
30天前
|
运维 关系型数据库
STM8S PWM互补输出加死区刹车配置指南
STM8S PWM互补输出加死区刹车配置指南
|
30天前
|
Kubernetes 网络安全 虚拟化
VCF Automation 9.1 发布 - 云计算自动化
formerly VMware Aria Automation | 为 AI、Kubernetes 以及基于虚拟机 (VM) 的应用交付自助式私有云
93 1
|
30天前
|
测试技术 API 开发工具
高QPS压测里,​D​М‌X​Α‌РΙ托住MiniMax调用
MiniMax-M2.7-highspeed 以高QPS、低延迟、强稳定性见长,专为高频生产场景设计。结合 DМXΑРΙ API 网关,提供认证、限流、重试、可观测等工程化能力,让模型真正成为可信赖的语义执行引擎。
|
1月前
|
前端开发 JavaScript 程序员
初级程序员必备的十大技能之开发工具熟练使用(三)
教程来源 https://bncne.cn/ 浏览器开发者工具是前端调试核心利器,涵盖Elements(实时编辑DOM/CSS)、Console(日志、断点、DOM操作)、Sources(多类型断点与作用域调试)、Network(请求分析与模拟)、Performance(性能指标与火焰图)及Application(存储管理)六大面板,全面提升开发效率。