【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知识点,百日打怪升级》


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

相关文章
|
18天前
|
人工智能 JSON 供应链
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
LucianaiB分享零成本畅用JVS Claw教程(学生认证享7个月使用权),并开源GeoMind项目——将JVS改造为科研与产业地理情报可视化AI助手,支持飞书文档解析、地理编码与腾讯地图可视化,助力产业关系图谱构建。
23533 12
畅用7个月无影 JVS Claw |手把手教你把JVS改造成「科研与产业地理情报可视化大师」
|
6天前
|
Shell API 开发工具
Claude Code 快速上手指南(新手友好版)
AI编程工具卷疯啦!Claude Code凭借任务驱动+终端原生的特性,成了开发者的效率搭子。本文从安装、登录、切换国产模型到常用命令,手把手带新手快速上手,全程避坑,30分钟独立用起来。
1713 10
|
3天前
|
人工智能 开发工具 iOS开发
Claude Code 新手完全上手指南:安装、国产模型配置与常用命令全解
Claude Code 是一款运行在终端环境中的 AI 编程助手,能够直接在命令行中完成代码生成、项目分析、文件修改、命令执行、Git 管理等开发全流程工作。它最大的特点是**任务驱动、终端原生、轻量高效、多模型兼容**,无需图形界面、不依赖 IDE 插件,能够深度融入开发者日常工作流。
1148 1
|
5天前
|
人工智能 JSON BI
DeepSeek V4-Pro 接入 Claude Code 完全实战:体验、测试与关键避坑指南
Claude Code 作为当前主流的 AI 编程辅助工具,凭借强大的代码理解、工程执行与自动化能力深受开发者喜爱,但原生模型的使用成本相对较高。为了在保持能力的同时进一步降低开销,不少开发者开始寻找兼容度高、价格更友好的替代模型。DeepSeek V4 系列的发布带来了新的选择,该系列包含 V4-Pro 与 V4-Flash 两款模型,并提供了与 Anthropic 完全兼容的 API 接口,理论上只需简单修改配置,即可让 Claude Code 无缝切换为 DeepSeek 引擎。
1217 0
|
12天前
|
人工智能 缓存 Shell
Claude Code 全攻略:命令大全 + 实战工作流(完整版)
Claude Code 是一款运行在终端环境下的 AI 编码助手,能够直接在项目目录中理解代码结构、编辑文件、执行命令、执行开发计划,并支持持久化记忆、上下文压缩、后台任务、多模型切换等专业能力。对于日常开发、项目维护、快速重构、代码审查等场景,它可以大幅减少手动操作、提升编码效率。本文从常用命令、界面模式、核心指令、记忆机制、图片处理、进阶工作流等维度完整说明,帮助开发者快速上手并稳定使用。
2771 4
|
3天前
|
人工智能 JSON BI
Claude Code 搭配 DeepSeek V4-Pro 完整测评:超越 Claude Sonnet 4.5,低成本高效能背后的真实表现
Claude Code 凭借强大的代码理解、工程执行与自动化任务能力,成为开发者广泛使用的 AI 编程工具。但原生模型的调用成本较高,长期高频使用会带来明显开销。DeepSeek V4 系列模型发布后,凭借优秀的代码能力与兼容 Anthropic 协议的 API 接口,成为替代原生模型的高性价比选择。本文完整记录将 Claude Code 对接 DeepSeek V4-Pro 的配置流程、真实任务测试效果、优势亮点与必须注意的使用限制,为开发者提供可直接落地的参考方案。
773 1
|
5天前
|
人工智能 Linux API
hermes agent 安装教程:安装优化 + 模型配置 + 工具启用指南
Hermes Agent 是 Nous Research 于 2026 年发布的开源自主进化 AI 智能体框架(MIT 协议,Python 编写)。它通过任务沉淀技能、持久化记忆、原生多工具集成与并行子智能体,实现“越用越强”。支持 Linux/macOS/WSL2,安装便捷,面向个人与企业的新一代私有化 AI 助手。