MySQL 的四种隔离级别

这篇只讲隔离级别 这一件事,不铺开讲 ACID、不讲三大日志、也不讲 InnoDB 的锁有多少种。需要提前知道的概念只有两个:事务 (一组要么全成要么全败的 SQL)和并发(多个事务同时在跑)。剩下的往下看就行。

一、四个级别,一级比一级严

SQL 标准定了四个隔离级别,从松到紧排下来是这样:

级别 英文 它挡住了什么
读未提交 READ UNCOMMITTED 什么都不挡
读已提交 READ COMMITTED 挡住脏读
可重复读 REPEATABLE READ 再挡住不可重复读
串行化 SERIALIZABLE 三个全挡住

你可能在别的地方见过"解决/未解决"的表格,但我建议换个记法:不要背表格,记"每一级往上加一道锁" 。级别越高,能读到的东西越少,加锁越多,并发越差。到这里其实就一句话------隔离性是用性能换来的 。

这里有个很多人不知道的点:四个级别不是"越来越高级",而是"场景不同"。

  • MySQL 的 InnoDB 默认是 可重复读(RR)
  • Oracle 和 PostgreSQL 默认都是 读已提交(RC)

同样一个业务,从 Oracle 迁到 MySQL,隔离级别悄悄变严了;反过来从 MySQL 迁到 Oracle,级别变松了,可能会出现以前没有的并发问题。这不是配置写错了,是两个数据库的默认哲学不一样。

至于为什么 MySQL 敢默认用更严的 RR,第五节给答案。


二、脏读:读到了别人会反悔的数据

先说最轻的那个。

假设有张账户表,张三余额 1000,现在事务 A 要给他扣 100:

复制代码
-- 会话 A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
UPDATE account SET balance = 900 WHERE id = 1;
-- 注意:这里还没 COMMIT

这时候事务 B 来查:

复制代码
-- 会话 B
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT balance FROM account WHERE id = 1;   -- 读到 900

B 读到了 900。但这 900 只是 A 改完还没提交的临时值。如果 A 接下来 ROLLBACK:

复制代码
-- 会话 A
ROLLBACK;

那个 900 就从来没有存在过。数据库里还是 1000,可是 B 已经拿着 900 去算账、扣款、写流水了。

这就是脏读。它的要害不是"读到了旧数据",而是读到了可能会消失的数据。

这个坑只要把级别提到"读已提交"就没了,因为读已提交规定只能读已经 COMMIT 的值。所以生产环境几乎没人真的开读未提交------它不是"更快",它是"没有任何保证"。

我一开始以为这个级别存在的意义是"给性能敏感的场景用",后来才明白,它更像是一个理论下界:先假设什么都不管会怎样,再一级级往上加约束。


三、不可重复读:同一个事务里两次读不一样

把级别提到读已提交,脏读没了。但还有个问题。

事务 B 开了个事务,要生成一份对账单,中间要读两次余额:

复制代码
-- 会话 B
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE id = 1;   -- 第一次:1000

就在这两次读之间,事务 A 把余额改了,而且正常提交了:

复制代码
-- 会话 A
BEGIN;
UPDATE account SET balance = 900 WHERE id = 1;
COMMIT;

然后 B 再读一次:

复制代码
-- 会话 B
SELECT balance FROM account WHERE id = 1;   -- 第二次:900
COMMIT;

同一个事务、同一句 SQL、中间什么都没干,结果从 1000 变成 900 了。

注意它跟脏读的本质区别:A 是提交了的,这 900 是真实生效的数据,一点都不脏。问题在于 B 自己的视角不一致------它开头看到的是一个世界,结尾看到的是另一个世界。

生成对账单的时候最怕这个:前面按 1000 算了一遍,后面按 900 又算一遍,最后两个数字对不上,你还找不到是哪行代码错了。

怎么解决? 让事务在整个过程中看到同一份数据。这就是可重复读(RR)干的事。

顺带说个容易误解的地方:不可重复读不是"不能读第二次",而是"两次读出来的不一样"。名字有点反直觉。


四、幻读:行数变了

再往上,可重复读。同一个事务里两次读到的一样了------但还有个更隐蔽的问题。

这次换个查法,不按主键查了,按条件查一批:

复制代码
-- 会话 B
BEGIN;
SELECT COUNT(*) FROM account WHERE age > 20;   -- 3 条

就在这中间,事务 A 插了一条新数据,年龄 25,也满足 age > 20:

复制代码
-- 会话 A
INSERT INTO account (id, name, balance, age) VALUES (4, '赵六', 500, 25);
COMMIT;

B 再查一次:

复制代码
-- 会话 B
SELECT COUNT(*) FROM account WHERE age > 20;   -- 还是 3

还是 3。因为 RR 用的是快照读,B 从头到尾看的是事务开始那一刻的数据。

看起来问题解决了?没有。你试试写一下:

复制代码
-- 会话 B
UPDATE account SET level = 1 WHERE age > 20;   -- 影响 4 行
SELECT COUNT(*) FROM account WHERE age > 20;   -- 变成 4 了

明明只"看见"3 条,一更新却改了 4 行。

这就是幻读最阴的地方:SELECT 是快照读,看见 3 条;UPDATE 是当前读,会去读最新版本,于是摸到了那第 4 条"看不见"的数据。改完之后,这条数据就进入 B 的视野了,再查变成 4 条。

像幻觉一样------你没动它,它自己冒出来了。

和不可重复读的分界线 :不可重复读是某一行的值变了 ,幻读是结果集的行数变了。一个是同一行前后不一致,一个是一个范围里凭空多/少了行。

RR 下 InnoDB 是怎么挡的?靠 next-key lock (记录锁 + 间隙锁)。你按 age > 20 去更新,它不只锁住命中那几行,还会把行与行之间的"空隙"一起锁住,别人想往这个范围里插数据就插不进来。

所以严格讲,MySQL 的 RR 已经基本解决了幻读。这也是它敢把默认级别设成 RR 的底气之一。


五、RC 和 RR 的差别,只有一个字:快照什么时候拍

到这里,四个级别讲完了三个。剩下最关键的一个问题:RC 和 RR 到底差在哪?

答案其实只有一句话------Read View 什么时候生成。

先说 Read View 是什么。你可以把它理解成一张"快照清单":在这一刻,记下哪些事务还没提交。之后读数据时拿这张清单去比对,没提交的版本就看不见。整个过程不加锁,靠版本链往回找------这就是 MVCC。

那这张快照什么时候拍?

  • RC:每次 SELECT 都重新拍一张。 所以每次查询都能看到"截至此刻已经提交"的最新数据。
  • RR:事务里第一次 SELECT 拍一张,之后一直用它。 所以整个事务看到的都是开头那一刻的世界。

拿第三节那个场景再走一遍,就一目了然了:同样中间有人提交了 900,RC 第二次读是 900(重拍了快照),RR 第二次读还是 1000(还用旧快照)。

这里有个细节很多文章写错,值得单独说:RR 的快照不是 BEGIN 的时候拍的,是第一次 SELECT 的时候拍的。

所以你 BEGIN 之后先干别的事、过一会儿再查,快照是那时候才拍的。如果你 BEGIN 完立刻 SELECT,那确实等于"事务开始那一刻";如果中间隔了很久,就不等于了。这个差别在长事务里会很明显。

还有一点:UPDATE / DELETE / SELECT ... FOR UPDATE 不走快照 ,它们叫"当前读",永远读最新版本。这就是第四节里 UPDATE 能改到 4 行的原因。

一句话总结这两级:

RC 是"每次查询看见最新的已提交数据",RR 是"整个事务看见的都是开始那一刻"。


六、怎么查、怎么改,以及到底该选哪个

查当前级别:

复制代码
SELECT @@transaction_isolation;
-- REPEATABLE-READ

(MySQL 8.0 之前这个变量叫 @@tx_isolation,8.0 改了名字,老教程里看到的写法可能跑不通。)

改的话分三种作用范围:

复制代码
-- 只影响当前连接
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 影响之后新建的连接(已连着的照旧)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 写进配置文件,重启也还在
SET PERSIST TRANSACTION ISOLATION LEVEL READ COMMITTED;

三个容易搞错的地方:

  1. 级别不是"整个库统一的",它是会话级的,每个连接可以不一样。你改了自己的,隔壁连接的代码完全不受影响。
  2. SET GLOBAL 改不了老连接。它只对之后新建的连接生效,已经连上的(比如连接池里那些)还是老级别。
  3. 改 my.cnf 不会立即生效 ,要重启。想不重启就持久化,用 SET PERSIST,它会写进 mysqld-auto.cnf。

那到底选哪个?我的建议很朴素:

场景 建议
课设、小项目、CRUD 别动,用默认的 RR
高并发读、死锁频繁 试着把某些会话改成 RC
对账、跑批、要绝对一致 Serializable
想省事 别碰 RU

默认的 RR 对绝大多数场景已经够用了,而且它比 RC 更"安全"------出问题的时候你的数据不会莫名其妙变来变去。真遇到死锁多、间隙锁锁范围太大这类问题,再考虑针对具体业务把个别会话改成 RC。

别一上来就全局改成 RC。RC 是 Oracle 的默认,不是 MySQL 的。


七、速查表

想搞清楚的问题 结论
四级从松到紧 RU → RC → RR → Serializable
MySQL 默认 RR(可重复读)
Oracle / PG 默认 RC(读已提交)
脏读的本质 读到了会回滚的数据
不可重复读的本质 同一事务里同一行,值变了
幻读的本质 同一事务里同一条件,行数变了
RC 和 RR 的差别 Read View 什么时候生成
RR 的快照什么时候拍 第一次 SELECT,不是 BEGIN
哪些操作不走快照 UPDATE / DELETE / SELECT ... FOR UPDATE
RR 怎么挡幻读 next-key lock(记录锁 + 间隙锁)
相关推荐
say_fall1 小时前
【Linux系统编程】Ext 文件系统:磁盘与存储、文件与目录、现代计算机究竟如何寻找文件
linux·运维·服务器·数据库·c++
码流子2 小时前
移动收费怎么做?手持终端+App+收费中心的架构实践
java·开发语言·架构
霸道流氓气质2 小时前
AI 流式对话与实时聊天平台:SSE 解码竞态、有状态会话隔离与背压取消的 Vue3 + Spring AI 生产级实战
java·人工智能·spring
云创智城-yuncitys2 小时前
YunParking 4.0:JDK 17 与 Spring Boot 3.2 的停车云升级
java·开发语言·spring boot
文丑颜不良啊2 小时前
LeetCode1661~1680题目解析
java·leetcode
子非鱼@Itfuture2 小时前
关于前后端联调中登录状态/登录信息失效?
java·前端
郭小李2 小时前
openFeign入门
java·开发语言·前端
共享家95272 小时前
408 操作系统第一章
服务器·数据库·windows
wljt2 小时前
公司二-面试题
java·开发语言·mybatis