故事场景:银行柜台与一本总账
把数据库想成一家银行的柜台:
- 总账 = 一张表里的数据:每笔账有多个历史版本,像账本上涂改后留下的旧字迹;
- 柜员 = 一个事务:从开始办业务到办完,是一个完整的过程,中间不能被人从手里抢走半截;
- 顾客 = 一条查询:来看一眼账上现在有多少钱。
麻烦出在:好几个柜员同时在办业务,有人正在改账、还没盖章确认,另一个人这时候来看账——他看到的,到底是改之前的数,还是改之后的数?四种隔离级别,就是四种不同的“看账规矩”。
先说清三个“看错账”的样子
这三个异常是理解隔离级别的钥匙,别只背名字:
- 脏读:A 正在改账、还没提交,B 就把 A 改到一半的数读走了。结果 A 后悔了、回滚了,B 手里那个数从来就没真实存在过——读到了脏数据。
- 不可重复读:同一个事务里,第一次读是 100,中间别人提交了一笔修改,第二次再读变成了 200。同一事务、同一条数据,前后两次结果不一样。
- 幻读:同一个事务里,第一次查“余额大于 100 的账户有 3 个”,中间别人新插入了一个符合条件的账户并提交了,第二次再查变成了 4 个——像凭空多出了一行。
隔离级别越往上,防住的异常越多,付出的代价是:并发越低、越容易互相等。
四种隔离级别,一张规矩表
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 一句话 |
|---|---|---|---|---|
| 读未提交(RU) | 可能 | 可能 | 可能 | 谁改到一半都能看,基本等于没隔离 |
| 读已提交(RC) | 不会 | 可能 | 可能 | 只看已提交的,但每次读都可能看到新提交 |
| 可重复读(RR) | 不会 | 不会 | 见下文 | 同一事务里看到的快照不变,InnoDB 默认就是它 |
| 串行化 | 不会 | 不会 | 不会 | 所有事务排队一个一个办,最安全也最慢 |
注意一个常考的细节:标准 SQL 说 RR 允许幻读,但 InnoDB 的 RR 在加锁读时用间隙锁(Next-Key Lock)把幻读也基本防住了——所以别把“标准”和“InnoDB 的实现”混着背,面试时点出这个区别很加分。
MVCC:为什么 RR 能让同一事务两次读到一样的数
InnoDB 的办法不是把所有人都锁在门外,而是给每行数据留“历史版本”,这就是 MVCC(多版本并发控制):
- 每行记录背后藏着两个字段:最后改它的事务号(trx_id) 和 指向上一个版本的指针(roll_pointer)。一次修改不是把旧数擦掉,而是新写一个版本、把旧版本链进回滚日志(undo log)里。
- 事务开始时,会拍一张 Read View(读视图):记下当时“已经开始但还没提交的事务名单”。判断某个版本能不能看,规则就一条——版本是这个快照之前就提交的,才能看;是快照之后才改的,看不见,就顺着版本链往旧里找。
- RC 和 RR 的全部区别,就在 Read View 拍几次:RC 是每条语句都重新拍一张,所以每次都可能看到别人刚提交的新数据;RR 是整个事务只拍一张,所以从头到尾看到的都是同一幅画面——这就是“可重复读”名字的由来。
再补一个边界:上面说的是普通读(快照读)。如果你用 SELECT ... FOR UPDATE 这类加锁读,InnoDB 会走当前读——直接看最新版本并加锁,这时防幻读靠的就是间隙锁,而不是快照。
面试一句话答案
- “RU、RC、RR、串行化防住的异常依次增多,并发依次降低。”
- “InnoDB 默认 RR,靠 MVCC 的 Read View 保证同一事务内读到一致快照,靠间隙锁在加锁读时防幻读。”
- “RC 每次语句新建 Read View、RR 一个事务只建一次,这是两者最本质的区别。”
和这篇搭着看:数据存在磁盘上怎么查得快,见《MySQL 索引为什么用 B+ 树?新华字典目录里藏着的秘密》;查出来的热数据怎么不每次都打到数据库,见《Redis 缓存三大问题:穿透、击穿、雪崩,小卖部一次讲清》。
动手验证:开两个 MySQL 会话,一个开事务改数据不提交,另一个分别在 RC 和 RR 下各读两次,对照着看结果变化,比背表格管用得多。练手需要一台自己的数据库机器时,可以看我整理的云服务器选购指南,新客首单很便宜,别买贵了。