脏读、不可重复读与幻读
了解三种读取异常,以及哪种隔离级别可以阻止它们。
脏读、不可重复读与幻读 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Coding Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Coding Interview Prep 课程共包含 4 节课。
三种读取异常
隔离级别用于防止特定的并发错误,这些错误称为读取异常。面试官希望您能够准确定义这三种异常,并将每种异常对应到能够阻止它的级别。
- 脏读 - 读取未提交的数据
- 不可重复读 - 两次读取之间某一行发生变化
- 幻读 - 两次读取之间出现了新行
难点在于区分不可重复读和幻读,因为两者都涉及重新查询并得到不同的结果。
脏读:定义
脏读发生在事务 T1 读取了事务 T2 已修改但尚未提交的行时。如果 T2 随后回滚,T1 就基于从未真正存在过的数据执行了操作。
只有 READ UNCOMMITTED 允许脏读。所有更高的隔离级别都会禁止脏读。
现实中的风险:根据一笔几秒后被回滚的存款批准贷款。
脏读:时间线
请将两列按时间线阅读。T1 在 READ UNCOMMITTED 下运行。
T1 看到余额为 700,但 T2 从未提交。这个 700 是 T2 尚未完成的工作产生的幻象。T2 回滚后,真实值仍为 500。T1 基于垃圾数据做出了决定。
-- T2 (not committed) | -- T1 (READ UNCOMMITTED)
BEGIN; |
UPDATE accounts |
SET balance = 700 |
WHERE id = 1; |
| SELECT balance FROM accounts
| WHERE id = 1; -- reads 700 (dirty!)
ROLLBACK; |
| -- T1 acted on a value that never existed不可重复读:定义
不可重复读发生在 T1 读取某一行后,T2 对同一行执行提交了的更新或删除,而 T1 再次读取时看到不同的值。
请注意它与脏读的关键区别:这里 T2 已经提交。数据是真实的,但在同一个事务期间,它在 T1 的操作过程中发生了变化。
READ COMMITTED 仍然允许这种情况。REPEATABLE READ 及更高的级别通过读取稳定的快照来防止它。
不可重复读:时间线
T1 在 READ COMMITTED 下运行,并两次读取同一行。在两次读取之间,T2 提交了一项更改。
同一个主键在一个事务中返回了两个不同的值。这种不一致会破坏那些假设该行保持稳定的多步骤逻辑。
-- T1 (READ COMMITTED) | -- T2
BEGIN; |
SELECT balance FROM accounts |
WHERE id = 1; -- 500 |
| BEGIN;
| UPDATE accounts SET balance = 900
| WHERE id = 1;
| COMMIT;
SELECT balance FROM accounts |
WHERE id = 1; -- 900 (changed!) |
COMMIT; |幻读:定义
幻读发生在 T1 执行带有搜索条件的查询后,T2 提交了对匹配该条件的行执行的 INSERT(或 DELETE),而 T1 重新运行查询时看到了不同的行集合。
它与不可重复读的区别在于:不可重复读关注的是现有行的值发生变化;幻读关注的是匹配某个谓词的行数发生变化。
按照标准,只有 SERIALIZABLE 能够保证防止幻读。
幻读:时间线
T1 两次统计高价值账户的数量。在两次统计之间,T2 插入了一条符合条件的新行并提交。
没有任何现有行发生变化,但 COUNT 却不同。新行就是出现在 T1 结果集中的“幻影”。
-- T1 (REPEATABLE READ, standard) | -- T2
BEGIN; |
SELECT COUNT(*) FROM accounts |
WHERE balance > 1000; -- 3 |
| INSERT INTO accounts(id, balance)
| VALUES (99, 5000);
| COMMIT;
SELECT COUNT(*) FROM accounts |
WHERE balance > 1000; -- 4 (phantom)|
COMMIT; |将异常映射到隔离级别
这种映射是本主题的核心。能够防止每种异常的最低级别如下:
- 脏读从
READ COMMITTED开始被防止。 - 不可重复读从
REPEATABLE READ开始被防止。 - 幻读由
SERIALIZABLE防止(依据标准)。
请注意这些名称是对应的:REPEATABLE READ 让读取变得可重复;这些级别的名称取自它们新解决的异常。
不可重复读与幻读:明确界线
这是面试中最常见的混淆点。请记住下面这句话:
不可重复读 = 现有行的值发生了变化。幻读 = 匹配的行集合发生了变化(增加或删除了行)。
请自行测试:T2 执行 UPDATE ... WHERE id = 5 后提交,T1 重新读取第 5 行。这是不可重复读。T2 执行 INSERT,插入一条与 T1 的 WHERE 匹配的新行,T1 重新运行查询。这是幻读。
写偏斜:额外的异常
高级面试可能会超出三种标准异常,考察写偏斜:两个事务各自读取一个有重叠的集合,根据读取结果执行互不相交的写入,随后都提交,导致最终状态是任何一个事务单独执行时都不会允许的状态。
经典例子是:两位医生值班;每位医生都确认另一位医生在岗,然后各自离开值班岗位。两次操作都成功,最终没有任何医生值班。
快照隔离(Postgres 的 REPEATABLE READ)允许写偏斜;只有 SERIALIZABLE 能够阻止它。提到这一点能体现理解的深度。
丢失更新:第四个陷阱
面试官有时会加入丢失更新,它不在标准的异常列表中,但在实践中非常常见。两个事务读取同一个值,都根据该值计算出新值,然后都写回结果。第二次写入会悄无声息地覆盖第一次写入。
例如:两笔转账都读取余额 500,各自扣除一笔金额,然后各自写入计算结果。其中一次扣除会丢失。
解决方案不只是提高隔离级别,还包括使用 SELECT ... FOR UPDATE 进行显式锁定,或者执行原子更新,让数据库而不是应用程序来计算结果。
-- Safe pattern: lock the row, or compute atomically
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- locks row
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- Or simply: UPDATE accounts SET balance = balance - 100 WHERE id = 1;快速检查
请根据行为识别异常类型。
总结:异常及其解决方案
三种读取异常,每种都可以通过更高的隔离级别解决:
- 脏读(未提交的数据) - 在 READ COMMITTED 级别得到解决。
- 不可重复读(现有行的值发生变化) - 在 REPEATABLE READ 级别得到解决。
- 幻读(匹配的行集合发生变化) - 在 SERIALIZABLE 级别得到解决。
请牢记不可重复读和幻读之间的明确界线;如果面试官想听更多,再提到写偏斜。接下来我们将了解数据库引擎实际上如何实现隔离:锁定、死锁和 MVCC。
常见问题解答
「脏读、不可重复读与幻读」课时是免费的吗?
是的 — 「脏读、不可重复读与幻读」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。
「脏读、不可重复读与幻读」这节课中我会学到什么?
了解三种读取异常,以及哪种隔离级别可以阻止它们。 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Coding Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「脏读、不可重复读与幻读」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Coding Interview Prep 课中编写并运行代码吗?
能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- ACID 属性详解
- 四种隔离级别
- 脏读、不可重复读与幻读
- 死锁、锁与 MVCC