深蓝黑背景封面:左侧大标题「同一条数据,两次读两个样」,副标题「三种读异常的定义、复现与隔离级别防线」;右侧原创示意图——两个事务时间线并排,上面的事务两次读同一行,第一次读到的卡片是实线、第二次变成虚线问号,中间另一事务插入一条修改箭头;左上标注「面试经典 · 数据库」,右上标注「Day 08 · 10-26」 图 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,多出一行幻影

三行时间线加防线表图:上方三条横向泳道各演示一种异常——脏读:T2 改了一行还没提交,T1 就读到了这个「可能随时回滚」的中间值;不可重复读:T1 第一次读是 100,T2 提交修改后,T1 在同一事务里第二次读变成 200;幻读:T1 按条件统计出 3 行,T2 插入一行符合条件的新数据并提交,T1 再数一遍变成 4 行,像凭空多出一行幻影。每条泳道右侧标注挡住它所需的最低隔离级别:脏读由读已提交挡住、不可重复读由可重复读挡住、幻读由可重复读的间隙锁或串行化挡住。底部对照表按四级隔离列出各自挡住的异常组合,并提示:隔离级别越高并发越低,选型是在正确性与吞吐之间取点 图 2:三种异常的复现时间线与各自的最低防线。

**把防线按级别摆一遍。**读未提交:三个异常全部放行,基本只出现在对一致性零要求的统计场景。读已提交:每次读都取最新已提交快照,脏读被挡住,但同一事务里快照换来换去,不可重复读和幻读都可能出现。可重复读(InnoDB 默认):同一事务的普通 SELECT 都读事务第一次读时建立的快照,不可重复读被挡住;范围写操作叠加间隙锁,幻读也被挡住。串行化:普通 SELECT 被隐式转成加共享锁的读,读写互斥,三个异常全挡,并发度也降到最低。

**常见误区。**一是把幻读和不可重复读混为一谈,判断口诀是「改已有行」还是「多出新行」;二是以为隔离级别越高越保险就一律开串行化,忽略了它把并发读写变成排队;三是以为默认级别下什么都不用管——快照读和加锁读看到的不是同一份数据,混用在一个事务里本身就是官方文档提醒的坑;四是只在面试里背级别,从没真正开两个会话复现过一次,异常长什么样全凭想象。

能复现,才谈得上选级别。建议每个人都在本地库里把上面三段 SQL 亲手跑一遍,把三个异常各制造一次——见过它们真实的样子,定义就再也忘不掉。你所在的业务把隔离级别定在哪一档,有没有被读异常咬过?欢迎分享。