这篇只讲隔离级别 这一件事,不铺开讲 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;

三个容易搞错的地方:
- 级别不是"整个库统一的",它是会话级的,每个连接可以不一样。你改了自己的,隔壁连接的代码完全不受影响。
SET GLOBAL改不了老连接。它只对之后新建的连接生效,已经连上的(比如连接池里那些)还是老级别。- 改
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(记录锁 + 间隙锁) |