大家好,我是十三!欢迎来到十三Tech。
前几年接手一个从 Oracle 迁移到 MySQL 的项目——开发以为 MySQL 的 RR(Repeatable Read)跟 Oracle 的 Serializable 等价,结果在压测时遇到"幻读"现象(同一事务里两次范围查询结果集不同),花了三天才搞明白根因。后来认真读了 SQL 标准和 InnoDB 的实现差别,才知道——MySQL 的 RR 不是 SQL 标准的 RR,它额外用间隙锁防了幻读。
这件事让我意识到,很多人讲隔离级别只讲"四个级别名字"——太浅。隔离级别真正要理解的是它防什么、不防什么、各自代价是什么。这一篇就聊聊 SQL 标准定义的三个异常、四个级别,以及 InnoDB 实现跟标准的差别。
一、SQL 标准 — 三种异常 + 四个级别
SQL 标准定义了三种"并发事务下可能出现的异常",按"严重程度"递减:
脏读(Dirty Read):事务 A 读到了事务 B 未提交的修改——B 后来回滚,A 读到的就是"从未存在过"的数据。生产中几乎不会遇到,因为最低级别(读未提交)都没人用。
不可重复读(Non-Repeatable Read):事务 A 两次读同一行,结果不同——因为中间事务 B 修改并提交了。注意是"同一行结果变化"。
幻读(Phantom Read):事务 A 两次执行同一范围查询,结果集行数不同——因为中间事务 B 插入或删除了符合条件的行。注意是"结果集行数变化",不是单行变化。
SQL 标准据此定义四个隔离级别:
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted(读未提交) | 可能 | 可能 | 可能 |
| Read Committed(读已提交) | 防止 | 可能 | 可能 |
| Repeatable Read(可重复读) | 防止 | 防止 | 可能 |
| Serializable(串行化) | 防止 | 防止 | 防止 |
这个表是 SQL 标准——很多教科书到这里就结束了。但InnoDB 的实现并不严格遵循这个表——这就是下一篇要讲的"坑"。
二、InnoDB 实现 — RR 实际上比标准强
InnoDB 对 SQL 标准做了"超额实现"——主要在 RR 级别上。
Read Uncommitted(RU):基本跟标准一致——允许脏读。生产没人用。
Read Committed(RC):跟标准一致——防止脏读,允许不可重复读和幻读。Oracle 默认就是这个级别。
Repeatable Read(RR)——InnoDB 在这里超额:标准说 RR 仍可能幻读,但 InnoDB 的 RR 用间隙锁防止幻读。这就是为什么开头那个 Oracle → MySQL 项目踩坑——开发以为 RR 会有幻读,实际没有。
Serializable:InnoDB 实现成"所有普通 SELECT 隐式转成 SELECT ... LOCK IN SHARE MODE"——所有读都加共享锁,串行执行。生产几乎不用——并发性太差。
-- 看当前隔离级别
SELECT @@transaction_isolation; -- 默认 RR
SHOW VARIABLES LIKE 'transaction_isolation';
InnoDB 不严格按标准实现的原因——SQL 标准是 1992 年的,当时的数据库还在用表锁实现并发;InnoDB 用 MVCC + 行锁 + 间隙锁,能用更低代价达到更严格的隔离。理解这一点很重要——InnoDB 的 RR 已经接近 Serializable 的效果,但性能好得多。这就是为什么 MySQL 默认 RR 而不是 RC——RR 的"防幻读"对业务更安全。
RR 不是完美——RR 下仍然有"写偏序"(Write Skew)等更微妙的异常。这些异常在 SQL 标准里没定义,但实际业务会遇到——A08 讲过几个例子。
三、怎么选 — 默认 RR 还是改 RC
讲完标准与实现的差别,再说生产中怎么选——这是工程师真正要做的决定。
MySQL 默认 RR 的理由:从库复制基于 binlog,早期 STATEMENT 格式在 RC 下会导致主从数据不一致(依赖事务顺序)。8.0 默认 ROW 格式后,RC 的复制问题消失了,但默认值还是 RR——历史包袱。
为什么很多互联网公司改 RC:
- 间隙锁代价:RR 下所有范围查询都加间隙锁,并发性降低。RC 下没有间隙锁,并发更好
- 死锁少:RR 的间隙锁是死锁高发区(A12 详细讲)。RC 几乎没有间隙锁死锁
- STATEMENT binlog 已不推荐:8.0 默认 ROW,RC 的复制问题不再存在
- 半同步复制 + ROW 已经解决了主从一致性问题
改 RC 的代价:
- 不可重复读:同一事务里两次读结果可能不同——业务要能接受
- 没有间隙锁保护:范围查询下的"插入"会"幻读"——业务要能接受
- 从库延迟可能更敏感:RC 下半同步复制的语义略有变化
判断口诀:
- 业务能接受"事务内不可重复读" + 追求高并发 → RC
- 业务需要"事务内严格一致" + 不追求极致并发 → RR
- 金融 / 强一致性场景 → RR + 显式 SELECT FOR UPDATE 兜底
- 永远不要用 Serializable 和 RU
回头看那个 Oracle → MySQL 项目——开发的误区是把"MySQL 的 RR"等同于"SQL 标准的 RR",没意识到 InnoDB 已经用间隙锁防了幻读。理解了 SQL 标准和 InnoDB 实现的差别后,那个压测里的"幻读现象"根因才浮现:那是 RC 下才会出现的现象,但他们的隔离级别是 RR——所以根因不是隔离级别,是某条 SELECT 用了 FOR UPDATE(当前读),当前读每次都看最新——才会出现"两次结果不同"。
隔离级别真正教会我的,不是"四个级别名词",而是所有"为什么我的查询结果不一样"的根因都在这里——理解标准与实现的差别,能解释 90% 的并发异常现象。
下一篇讲锁——它是隔离级别背后的真正执行机制。
关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。
联系方式:569893882@qq.com GitHub:@TriTechAI
