图 1:本文封面,两次读两个样。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。脏读、不可重复读、幻读这三个词几乎人人会背,但面试官真正想看的,是你能不能各写出一个复现过程——说不清「长什么样」的定义,等于没定义。
先把结论立住:三个异常按三件事区分——读到别人还没提交的值(脏读)、同一行两次读结果不同(不可重复读)、同一个条件两次数数结果不同(幻读)。隔离级别就是防线:读未提交什么都不挡,读已提交挡脏读,可重复读(InnoDB 的默认级别)挡前两种、并靠间隙锁挡住幻读,串行化全挡。级别越高,并发越低,选型是在正确性和吞吐之间取点。
**脏读:读到「可能随时回滚」的中间值。**T2 把一行从 100 改成 200,还没提交;T1 这时候读到 200 并据此做了决策;随后 T2 回滚,那个 200 从未真实存在过。按 MySQL 官方文档的说法,读未提交级别下 SELECT 可能用到行的旧版本或未提交的版本,「这也被称为脏读」。复现只需要两个会话:
-- 会话 T2 -- 会话 T1(隔离级别:读未提交)
START TRANSACTION;
UPDATE account SET balance = 200 WHERE id = 1;
-- 故意不提交
SELECT balance FROM account WHERE id = 1;
-- 读到 200
ROLLBACK; -- 那个 200 从未存在过
**不可重复读:同一事务里,同一行两次读不一样。**T1 第一次读 balance 是 100;T2 把这一行改成 200 并提交;T1 还没结束,再读一次,变成 200 了。「不可重复」指的就是同一个查询在同一事务里不能重演出同样的结果。它发生在读已提交级别——官方文档明确:该级别下每一次一致性读都读取自己那份全新的快照,所以同事务里两次 SELECT 看到的是不同时间点的世界。
-- 会话 T1(隔离级别:读已提交) -- 会话 T2
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 100
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
SELECT balance FROM account WHERE id = 1; -- 200,同一事务里结果变了
幻读:同一事务里,同一个条件两次数,行数变了。T1 统计余额大于 50 的账户有 3 行;T2 插入一行符合条件的新数据并提交;T1 再数一遍,4 行——多出来的一行像幻影。它和不可重复读的区别要咬死:不可重复读是已有行被改,幻读是符合条件的新行冒出来。防线也不同:InnoDB 在可重复读级别对范围条件加间隙锁、临键锁,把别人插进这个范围的路堵死;而读已提交级别禁用了间隙锁,官方文档原话是「幻读问题可能发生,因为其他会话可以往间隙里插新行」。
-- 会话 T1 -- 会话 T2
START TRANSACTION;
SELECT COUNT(*) FROM account WHERE balance > 50; -- 3
INSERT INTO account VALUES (9, 80);
COMMIT;
SELECT COUNT(*) FROM account WHERE balance > 50; -- 4,多出一行幻影
图 2:三种异常的复现时间线与各自的最低防线。
**把防线按级别摆一遍。**读未提交:三个异常全部放行,基本只出现在对一致性零要求的统计场景。读已提交:每次读都取最新已提交快照,脏读被挡住,但同一事务里快照换来换去,不可重复读和幻读都可能出现。可重复读(InnoDB 默认):同一事务的普通 SELECT 都读事务第一次读时建立的快照,不可重复读被挡住;范围写操作叠加间隙锁,幻读也被挡住。串行化:普通 SELECT 被隐式转成加共享锁的读,读写互斥,三个异常全挡,并发度也降到最低。
**常见误区。**一是把幻读和不可重复读混为一谈,判断口诀是「改已有行」还是「多出新行」;二是以为隔离级别越高越保险就一律开串行化,忽略了它把并发读写变成排队;三是以为默认级别下什么都不用管——快照读和加锁读看到的不是同一份数据,混用在一个事务里本身就是官方文档提醒的坑;四是只在面试里背级别,从没真正开两个会话复现过一次,异常长什么样全凭想象。
能复现,才谈得上选级别。建议每个人都在本地库里把上面三段 SQL 亲手跑一遍,把三个异常各制造一次——见过它们真实的样子,定义就再也忘不掉。你所在的业务把隔离级别定在哪一档,有没有被读异常咬过?欢迎分享。