幻读与写偏斜

共 19 题
📑 题目列表 19 题
#
★★★

1. REPEATABLE READ 下幻读的防止,Next-Key Lock 或快照读?

在 REPEATABLE READ 隔离级别下,幻读是如何被防止的?是靠 Next-Key Lock 还是快照读?

  • 快照读与当前读下幻读的差异
  • Next-Key Lock 的作用
  • RR 下幻读的防止机制

在 REPEATABLE READ 下,幻读的防止是"快照读 + 临键锁"共同作用的结果。对于快照读(普通 SELECT),通过事务级 Read View 的复用,同一事务内两次查询返回一致的行集合,因此看不到"幻行"。对于当前读(SELECT ... FOR UPDATE、UPDATE、DELETE),InnoDB 使用 Next-Key Lock(临键锁,即记录锁 + 间隙锁的组合)锁定搜索范围内的记录及其间隙,阻止其他事务在范围内插入新行,从而防止幻读。所以快照读靠 MVCC 版本隔离,当前读靠临键锁。

关键要区分"快照读防幻读"与"当前读防幻读"是两种不同的机制。快照读靠 Read View 复用,当前读靠锁。这也是为什么 RR 下普通 SELECT 不会看到幻行,而若不加锁则可能在某些场景下(如当前读)暴露幻读问题。

#
★★★

2. 写偏斜的检测,PostgreSQL SSI 在 SERIALIZABLE 级别下捕获?

PostgreSQL 是如何在 SERIALIZABLE 级别下通过 SSI(串行化快照隔离)检测写偏斜的?

  • 写偏斜的概念
  • SSI 机制
  • rw-antidependency 与冲突检测

写偏斜指两个事务各自基于读到的快照做出判断后写入冲突数据,但每个事务都未修改对方读过的那行,因而传统锁无法阻止,两个事务都能提交。PostgreSQL 的 SERIALIZABLE 级别采用 SSI(Serializable Snapshot Isolation)机制,它跟踪事务之间的读写依赖(rw-antidependency),当检测到读写依赖形成环时,判定存在写偏斜,会中止其中一个事务回滚,从而保证可串行化。

SSI 的核心是"先让事务并发执行,提交时检查是否存在会破坏可串行化的依赖环"。它允许读写并发,只在检测到环时中止,因此比全加锁的 SERIALIZABLE 并发度更高。这是 PostgreSQL 相比传统锁实现的重要优势。

#
★★★

3. 写偏斜(Write Skew)的定义,两个事务基于各自读到的快照写入冲突但各自提交?

什么是写偏斜(Write Skew)?为什么两个事务基于各自读到的快照写入冲突数据却都能各自提交?

  • 写偏斜的定义
  • 与丢失更新的区别
  • 基于快照读的写入

写偏斜指两个并发事务各自基于自己读到的快照做判断,然后写入可能互相冲突的数据,但两个事务写入的是不同的行(或不同的字段),因此传统行锁不会让它们互相阻塞,最终两个事务都能成功提交,破坏业务约束。它与丢失更新不同:丢失更新是"写同一行互相覆盖",写偏斜是"写不同行但违反约束"。由于每个事务都只锁了自己写入的行,没有锁对方读过的行,所以无法在并发时阻止冲突。

写偏斜的根源在于"基于陈旧快照做判断 + 写入不同行"。行锁只能串行化对同一行的写,无法处理"写不同行但逻辑约束冲突"的场景。理解写偏斜需要跳出"同一行"的思维,关注"跨行约束"。

#
★★★

4. 幻读与不可重复读的根本差异,行集合变化 vs 行属性变化?

幻读与不可重复读的根本差异是什么?行集合变化与行属性变化分别指什么?

  • 幻读的定义
  • 不可重复读的定义
  • 两者的差异

不可重复读指同一事务内两次读取同一行,行内容(属性)发生变化,即"行属性变化";幻读指同一事务内两次执行同一查询,返回的行集合发生变化(出现了新的行或少了行),即"行集合变化"。根本差异在于:不可重复读关注的是已有行的值变化,幻读关注的是查询结果集中行的数量/成员变化。两者都源于读一致性被破坏,但维度和应对机制不同。

区分两者的关键是"变了什么":值变了是不可重复读,行的增删导致了集合变化是幻读。在 RR 下快照读能同时避免两者,但当前读要防幻读需临键锁,防不可重复读需记录锁。

#
★★★

5. InnoDB 在 RR 下如何用 Next-Key Lock 防止幻读,对索引记录加间隙锁/临键锁的范围与死锁代价?

InnoDB 在 RR 下如何用 Next-Key Lock 防止幻读?它对索引记录加间隙锁/临键锁的范围是什么?其死锁代价是什么?

  • Next-Key Lock 的组成
  • 加锁范围
  • 死锁代价

Next-Key Lock 是记录锁(Record Lock)与间隙锁(Gap Lock)的组合,锁住"索引记录及其前面的间隙"。当前读时,InnoDB 会对查询范围内命中的索引记录加记录锁,并对记录之间的间隙加间隙锁,从而阻止其他事务在间隙内插入新行,防止幻读。其加锁范围取决于索引:若走唯一索引等值查询,RR 下可能退化为仅记录锁;若走普通索引或范围查询,则加临键锁覆盖整个扫描范围。死锁代价是:间隙锁范围较大、锁冲突增多,且锁的粒度和范围扩大使并发度下降,同时锁等待增多容易形成死锁。

临键锁是 RR 防幻读的关键,但代价是锁范围大、并发低、易死锁。这也是为什么很多生产环境选择 RC 隔离级别(RC 不加间隙锁,只加记录锁)以提升并发,代价是可能存在幻读(靠应用层或锁处理)。

#
★★★

6. 为什么 MVCC 快照读看到旧版本而当前读看到最新版本,二者混用(先查后改)会破坏哪些一致性假设

为什么 MVCC 快照读看到旧版本而当前读看到最新版本?二者混用(先查后改)会破坏哪些一致性假设?

  • 快照读与当前读的可见性差异
  • 先查后改的业务模式
  • 被破坏的一致性假设

快照读基于事务级 Read View 看到固定的历史版本,当前读基于当前时刻看到最新已提交版本。二者混用(先 SELECT 判断,再 UPDATE/DELETE)时,判断基于旧快照,但写入基于最新数据,若中间有其他事务提交,就会破坏"判断与写入基于同一份数据"的一致性假设。典型后果是丢失更新(覆盖他人的修改)和写偏斜(基于过期判断写入冲突数据)。此外还会破坏"读到的即是我要改的"这一前提。

"先查后改"是丢失更新和写偏斜的共同根源。要避免,应让"判断"与"写入"基于同一版本:要么把判断也做成当前读(SELECT ... FOR UPDATE),要么用条件更新(UPDATE ... WHERE 状态=期望值)把判断下沉到写入语句。

#
★★

7. 幻读(Phantom Read)的定义,同一查询返回不同行集?

什么是幻读(Phantom Read)?它的核心特征是什么?

  • 幻读的定义
  • 行集合变化
  • 与不可重复读的区别

幻读指在同一事务内执行两次相同的查询,返回的行集合发生变化——即出现了第一次查询时不存在的新行,或消失了第一次查询时存在的行。这种"凭空出现/消失"的行被称为幻行。幻读的关键特征是行集合(数量或成员)变化,而非行内容变化。它主要由其他事务的插入/删除操作引起。

幻读的核心是"行集合变化"。理解这个定义有助于区分"读同一行值变了"(不可重复读)和"读出了不同的行"(幻读)。在 RR 下快照读可避免幻读,当前读需靠临键锁。

#
★★

8. 写偏斜的解决,SERIALIZABLE 隔离、显式锁、应用层约束?

写偏斜可以通过哪些方式解决?SERIALIZABLE 隔离、显式锁、应用层约束分别如何起作用?

  • SERIALIZABLE 隔离
  • 显式锁(FOR UPDATE / 锁表)
  • 应用层约束

解决写偏斜可以:一是提升隔离级别到 SERIALIZABLE,让数据库保证可串行化(SQL Server 用锁,PostgreSQL 用 SSI 检测并中止冲突事务);二是显式加锁,把相关行用 SELECT ... FOR UPDATE 锁住,或锁表,使"判断 + 写入"串行化;三是在应用层施加约束,用唯一约束、条件更新、约束触发器从数据层面让冲突不可能发生。三者可组合使用。

写偏斜的难点在于"跨行约束",行锁无法直接解决。SERIALIZABLE 从数据库层面保证,显式锁从应用层强制串行,应用层约束从数据设计上杜绝冲突。选择哪种取决于并发度与一致性要求。

#
★★

9. 快照读 vs 当前读下的幻读,为什么 RR 快照读看不到幻行、但当前读(SELECT FOR UPDATE)可能看到?

在快照读与当前读下,幻读的表现有何不同?为什么 RR 下快照读看不到幻行,但当前读(SELECT FOR UPDATE)可能看到?

  • 快照读防幻读
  • 当前读的幻读
  • 加锁读的语义

RR 下快照读通过复用事务级 Read View,看不到事务开始后新插入且已提交的行,因此看不到幻行。但当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)读取的是最新已提交数据并加锁,如果其他事务在范围内插入了新行并提交,当前读会看到这些新行。不过 InnoDB 在 RR 下对当前读会加临键锁,阻止其他事务在锁定范围内插入,因此正常情况下当前读也不会看到幻行;仅当锁定范围未覆盖插入位置(如插入到间隙之外)时可能暴露。理论上靠"临键锁"防止当前读的幻读。

快照读防幻读靠 Read View 复用,当前读防幻读靠临键锁。题干强调"SELECT FOR UPDATE 可能看到"是考察对"当前读读最新数据"这一语义的理解,以及临键锁覆盖范围的重要性。

#
★★

10. 为什么数据库无法自动防止写偏斜?业务侧如何用唯一约束、约束触发器或提升隔离级别系统性兜底?

为什么数据库无法自动防止写偏斜?业务侧如何用唯一约束、约束触发器或提升隔离级别系统性兜底?

  • 写偏斜的根源
  • 唯一约束
  • 约束触发器与隔离级别

数据库无法在默认隔离级别(RC/RR)下自动防止写偏斜,因为写偏斜涉及对不同行的约束,而默认的快照隔离与行锁只保证单行写入的串行,无法推断并阻止"跨行约束被违反"。要系统性兜底,业务侧可:用唯一约束强制某些状态唯一,从数据层面杜绝冲突;用约束触发器(AFTER/BEFORE TRIGGER)在写入时校验跨行约束,违反则拒绝;或提升隔离级别到 SERIALIZABLE,让数据库检测并中止破坏可串行化的冲突写。

数据库默认不防写偏斜的本质是"约束知识在业务侧"。业务要主动把约束表达给数据库(唯一约束、触发器),或要求数据库做可串行化保证(SERIALIZABLE)。理解"约束在哪一层"是设计一致性方案的关键。

#
★★

11. 写偏斜的经典场景,值班排班、余额互转、库存双写,为什么加行锁无法解决、需要串行化或显式锁表?

写偏斜的经典场景有哪些(值班排班、余额互转、库存双写)?为什么加行锁无法解决,需要串行化或显式锁表?

  • 写偏斜的经典场景
  • 行锁的局限
  • 串行化与锁表

经典场景包括:值班排班中两个事务各自判断"至少还剩一人值班"后同时把最后两人都下线;余额互转中两个账户同时转出各自判断余额充足;库存双写中两个写操作各自基于快照判断库存。这些场景的问题是:两个事务读取的是不同的行(或不同字段),各自加行锁只锁了自己要写的行,锁不住对方判断依据的行,从而无法阻止冲突。解决需将整个判断与写入串行化:用 SELECT ... FOR UPDATE 锁住所有相关行,或锁表,或提升到 SERIALIZABLE 让数据库检测。

行锁只能锁"我要写的行",写偏斜的冲突在"我读到的约束"上。要让约束判断与写入原子化,必须把读到的判断依据也锁住或整体串行化,这正是行锁不够、需要锁表/串行化/SSI 的原因。

#
★★

12. RR 下当前读的幻读,SELECT FOR UPDATE 与临键锁?

在 RR 下,当前读(SELECT FOR UPDATE)是如何通过临键锁防止幻读的?

  • 当前读
  • 临键锁
  • 防幻读机制

在 RR 下,SELECT ... FOR UPDATE 是当前读,会读取最新已提交数据并加锁。为了防止幻读,InnoDB 会为查询范围内命中的索引记录加 Next-Key Lock(记录锁 + 间隙锁),锁定这些记录及其前面的间隙,从而阻止其他事务在锁定的间隙内插入新行。这样在事务提交前,范围内不会出现新的可插入位置,也就不会产生幻行。若走唯一索引等值查询并命中,RR 下可退化为仅记录锁。

临键锁是当前读防幻读的关键。它把"记录 + 间隙"一起锁住,使范围内的插入被阻塞。理解临键锁的作用与适用条件(索引唯一性、等值/范围)有助于分析 RR 下的锁行为与死锁。

#
★★

13. SSI 检测写偏斜的机制,rw-antidependency 与提交时冲突检查

SSI 检测写偏斜的机制是什么?rw-antidependency 与提交时冲突检查是如何工作的?

  • SSI 机制
  • rw-antidependency
  • 提交时冲突检查

SSI(Serializable Snapshot Isolation)通过跟踪事务间的读写依赖(rw-antidependency)来检测写偏斜。当一个事务 T1 读取了某个数据,而另一个事务 T2 随后写入并可能影响该数据(即 T1 的读与 T2 的写存在 antidependency),SSI 会记录这种依赖。在事务提交时,SSI 检查这些依赖是否形成环;若形成环,说明事务序列无法对应任何串行执行顺序,判定存在写偏斜,会中止其中一个事务并回滚,从而保证可串行化。

SSI 的核心思想是"乐观 + 提交时验证":允许读写并发,靠跟踪依赖并在提交时检测环来识别冲突。相比锁实现,它不阻塞读写,因此在高并发读多写少场景下并发度更高,代价是可能在中途被中止(增加重试)。

#
★★

14. 业务层如何设计约束使写偏斜不可能发生(冗余列、唯一约束、锁表)

业务层如何设计约束使写偏斜不可能发生?冗余列、唯一约束、锁表分别如何运用?

  • 冗余列
  • 唯一约束
  • 锁表

业务层可通过数据设计让写偏斜不可能发生:一是冗余列,把需要共同维护的状态冗余到一个字段,使并发写退化为"写同一行/同一字段",从而被行锁串行化;二是唯一约束,对约束冲突的列加唯一索引,从数据层面强制唯一,重复插入会被拒绝;三是锁表,在事务内对相关表加锁(LOCK TABLE ... 或 SELECT ... FOR UPDATE 覆盖所有相关行),使判断与写入串行化。这些手段把"跨行约束"转化为"单行写"或"数据库强制约束",从而消除写偏斜。

写偏斜的本质是"跨行约束无法被行锁保护"。通过冗余列/唯一约束把约束下沉到数据库可强制检查的程度,或用锁表把相关操作串行化,就能使写偏斜在结构上不可能发生。这是系统性兜底而非临时补丁。

#
★★

15. 隔离级别与一致性的完整矩阵,脏读、不可重复读、幻读与写偏斜在各隔离级别下是否可能发生,SERIALIZABLE 如何一并消除?

隔离级别与一致性的完整矩阵是怎样的?脏读、不可重复读、幻读与写偏斜在各隔离级别下是否可能发生?SERIALIZABLE 如何一并消除?

  • 隔离级别矩阵
  • 各异常发生的条件
  • SERIALIZABLE 的消除

标准矩阵为:READ UNCOMMITTED 可能发生脏读、不可重复读、幻读;READ COMMITTED 消除了脏读,仍可能发生不可重复读和幻读;REPEATABLE READ 消除了脏读和不可重复读,但理论上仍可能发生幻读(MySQL 的 RR 通过临键锁也大概率消除);SERIALIZABLE 消除上述所有异常。写偏斜属于快照隔离(RC/RR,即 MySQL 的 RR 快照隔离)特有的问题,传统 SERIALIZABLE 或需通过 SSI 机制消除。SERIALIZABLE 通过串行化事务(锁或 SSI 检测)保证任一并发执行等价于某种串行顺序,从而一并消除脏读、不可重复读、幻读和写偏斜。

矩阵是面试常考点。注意:MySQL 的 RR 在快照读和当前读下都通过 MVCC + 临键锁消除了幻读,与标准略有差异;写偏斜在快照隔离下存在,需 SERIALIZABLE(尤其 SSI)消除。理解矩阵要结合"行锁只能防同行冲突、还需防跨行与集合冲突"。

#
★★

16. 应用层对写偏斜的兜底,在无法使用 SERIALIZABLE 时,如何用唯一约束、条件更新与锁升级在应用层防止约束违背?

在无法使用 SERIALIZABLE 时,应用层如何用唯一约束、条件更新与锁升级防止约束违背(写偏斜)?

  • 唯一约束
  • 条件更新
  • 锁升级

无法使用 SERIALIZABLE 时,应用层可:用唯一约束把冲突列强制唯一,重复写入被数据库拒绝;用条件更新(UPDATE ... WHERE 条件=期望值)把判断下沉到原子写入,使"检查 + 更新"成为一条原子语句,若影响行数为 0 则说明状态已变化,需重试或报错;用锁升级(SELECT ... FOR UPDATE 锁住判断依据的相关行,或锁表)把跨行冲突串行化。这些手段在数据库层面消除"基于陈旧快照的跨行写入"。

条件更新是应用层防写偏斜的经典手法:把"先查后写"改成"原子条件写",让判断和写入合并为一条语句,由数据库保证原子性。唯一约束则从数据层面杜绝重复。锁升级牺牲并发换取正确性。

#
★★

17. 快照隔离的提交冲突,为什么 SI 下两个并发事务同时写同一行时后提交者会失败,与悲观锁冲突相比的体验差异?

在快照隔离下,两个并发事务同时写同一行时为什么后提交者会失败?这与悲观锁冲突的体验有何差异?

  • 快照隔离的提交冲突
  • 先写者胜
  • 与悲观锁的体验差异

在快照隔离(SI)下,两个事务同时读取同一行并各自修改,若二者都写同一行,后提交者在提交时会被数据库判定为"first-committer-wins"冲突而失败(中止),因为它的写入基于陈旧快照,覆盖了先提交者的修改。这与悲观锁体验不同:悲观锁下,事务在写操作执行时就获取锁,冲突的执行者会立即阻塞等待(等锁),直到前一个事务提交或超时,然后基于新值继续;而快照隔离下,两个事务都能继续执行,直到提交时才有一方失败,需应用层重试。

快照隔离的冲突是"提交时才发现",乐观;悲观锁是"执行时阻塞",先等再写。快照隔离失败是"中止 + 重试",悲观锁是"等待 + 经过"。理解这种差异对设计重试策略非常重要。

#
★★

18. 串行化快照隔离(SSI)的原理,如何通过读写依赖跟踪检测冲突环,相比锁实现的并发度优势?

串行化快照隔离(SSI)的原理是什么?如何通过读写依赖跟踪检测冲突环?相比锁实现的并发度优势是什么?

  • SSI 原理
  • 读写依赖跟踪
  • 与锁实现的并发度对比

SSI 的原理是"乐观并发 + 提交时验证":事务在快照隔离下并发执行,数据库持续跟踪事务之间的读写依赖(rw-antidependency)。当某个事务的读写依赖与另一个事务形成环时,说明该并发执行无法对应任何串行顺序,SSI 会在提交时判定存在写偏斜并中止其中一个事务。相比锁实现(如 SQL Server 的 SERIALIZABLE,通过封锁读/写范围),SSI 不阻塞读写操作,允许读写并发进行,只在提交时检测冲突,因此在高并发读多写少场景下并发度更高,吞吐更大;代价是事务可能在中途被中止回滚,需要应用层重试。

SSI 的核心取舍是"用可能的中止换取并发度",而锁实现是"用阻塞换取不会中止"。SSI 适合冲突率低、读多写少的场景;冲突率高时会因频繁中止而性能下降。理解这一取舍是 SSI 的要点。

#

19. 幻读的工程场景,库存区间统计、报表行数不一致,如何用 SERIALIZABLE 或加锁读解决?

幻读的工程场景有哪些(库存区间统计、报表行数不一致)?如何用 SERIALIZABLE 或加锁读解决?

  • 幻读的工程场景
  • SERIALIZABLE 解决
  • 加锁读解决

幻读的工程场景包括:库存区间统计(两个事务同时统计某价格区间的库存数量,期间插入新库存记录导致统计数字变化)、报表行数不一致(同一报表事务内两次 COUNT 结果不同)。解决方式:用 SERIALIZABLE 隔离级别让数据库保证事务可串行化;或对相关查询加锁读(SELECT ... FOR UPDATE / FOR SHARE),配合临键锁(RR 下)阻止并发插入,使统计期间范围内行集合稳定。对于统计类需求也可用 RR 快照读(快照读下看不到幻行)。

幻读影响的是"行集合"相关统计的正确性。SERIALIZABLE 从数据库层保证,加锁读从应用层主动锁范围。RR 下快照读本身对普通 SELECT 已消除幻读,但当前读/加锁读需靠临键锁。