脏读与不可重复读

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

1. READ COMMITTED 下不可重复读的发生,每条语句看到最新已提交?

在 READ COMMITTED 隔离级别下,为什么会出现不可重复读?为什么每条语句都看到最新已提交的数据会导致同一事务内两次读取结果不一致?

  • READ COMMITTED 的读一致性语义
  • Read View 的生成时机
  • MVCC 快照的生命周期

READ COMMITTED 隔离级别下,每个 SELECT 语句在执行时都会新建一个 Read View(快照),而不是整个事务只生成一个快照。因此,事务 A 中第一条 SELECT 看到的是该语句开始时已提交的数据;如果另一个事务 B 在两条 SELECT 之间提交了对同一行数据的修改,事务 A 的第二条 SELECT 会基于新的 Read View 看到 B 提交的新数据。由于两次语句生成的快照不同,读到的数据也就不一致,这就产生了不可重复读(Non-Repeatable Read)。

不可重复读的本质是同一事务内两次读取同一行数据,但行内容发生了变化。RC 选择"每次语句刷新快照"的取舍,是为了让每次读都能尽快看到最新提交的数据,牺牲了事务内的一致性,换取了更低的隔离代价和更高的并发度。理解这一点是区分 RC 与 REPEATABLE READ 的关键。

#
★★★

2. READ UNCOMMITTED 下脏读的发生,PG 无脏读,MySQL 也实现无脏读?

READ UNCOMMITTED 隔离级别下为什么会发生脏读?为什么 MySQL 会产生脏读而 PostgreSQL 不会?

  • 脏读的定义
  • MySQL 与 PostgreSQL 对 READ UNCOMMITTED 的实现差异
  • 加锁读与快照读的机制

脏读是指一个事务读取到另一个事务尚未提交的数据。理论上 READ UNCOMMITTED 允许读取未提交的数据,因此会产生脏读。两大数据库的实现差异:MySQL 的 InnoDB 在 READ UNCOMMITTED 下,普通 SELECT 直接读取记录的最新版本(不通过 Read View 做可见性过滤),因此会读到其他事务未提交的修改,确实会产生脏读,这也是官方文档将该级别称为 dirty read 的原因;PostgreSQL 的 READ UNCOMMITTED 被实现为等价于 READ COMMITTED,其 MVCC 机制天然只让事务读取已提交的版本,因此不会产生脏读。

是否产生脏读取决于数据库对 READ UNCOMMITTED 的具体实现:InnoDB 在 RU 下绕过可见性检查直接读最新记录版本,因此会脏读;PostgreSQL 把 RU 实现为 RC 语义,快照机制过滤未提交修改,因此不脏读。理解这一点要结合数据库的具体实现,而不能只停留在 SQL 标准对隔离级别的文字定义。

#
★★★

3. 不可重复读(Non-Repeatable Read)的定义,同一事务中两次读取结果不同?

什么是不可重复读(Non-Repeatable Read)?它的核心特征是什么?

  • 不可重复读的定义
  • 与脏读、幻读的区别
  • 行内容变化 vs 行集合变化

不可重复读指在同一事务内,两次读取同一行数据,得到的内容不一致。第一次读取时获取了一个值,随后另一个事务提交了修改,事务内第二次读取同一行时得到的是修改后的新值。其核心特征是"同一行数据的内容发生变化",且只发生在其他事务提交了修改之后(与脏读不同,脏读读到的是未提交数据)。MVCC 的 REPEATABLE READ 通过持续使用同一快照来避免不可重复读。

不可重复读关注的是"一条记录的值"变化,而幻读关注的是"查询结果的行集合"变化。题目常把二者混淆,需牢记:不可重复读是行内容变化,幻读是行数量/行集合变化。

#
★★★

4. MVCC 下的快照读与当前读,为什么 RC/RR 的快照读不产生脏读,当前读(加锁读)看到的是最新已提交?

在 MVCC 机制下,为什么 RC/RR 下的快照读不会产生脏读?为什么当前读(加锁读)看到的是最新已提交的数据?

  • 快照读与当前读的概念
  • Read View 与 undo log 版本链
  • 当前读加锁语义

快照读(普通 SELECT)基于 Read View 和 undo log 版本链,只读取在 Read View 生成时可见的已提交版本,未提交事务的修改对快照读不可见,因此不会产生脏读。当前读(SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT 等)读取的是最新已提交的数据,并且会加锁(在 RC 下加记录锁,在 RR 下可能加临键锁),所以它总是看到当前最新已提交版本,也因此可能读到其他事务刚提交的数据。快照读不加锁、利用版本链,当前读加锁、读最新版本,这是两者最本质的区别。

快照读保证读一致性靠的是"版本隔离",当前读保证正确性靠的是"加锁"。在当前读返回最新已提交数据这一语义下,它不会读到未提交数据,所以也不会产生脏读。理解"快照读 vs 当前读"是理解 MVCC 并发控制的核心。

#
★★★

5. 当前读(加锁读)在 RC/RR 下读取最新已提交数据,与快照读混用时的数据一致性边界

当前读(加锁读)在 RC/RR 下读取最新已提交数据,那么与快照读混用时,数据一致性的边界在哪里?

  • 快照读与当前读混用的语义
  • 一致性边界的理解
  • 读写冲突场景

快照读看到的是事务内固定快照下的历史版本,而当前读看到的是执行时刻的最新已提交数据。当两者混用时,同一事务内先做快照读、再做当前读,可能读到不同版本的数据,从而破坏事务内的读一致性假设。例如事务内先 SELECT 统计,再 SELECT FOR UPDATE 更新,前者基于旧快照,后者基于最新数据,两者之间若有其他事务提交就会不一致。因此一致性边界是:快照读保证"事务内读一致",当前读保证"读到最新已提交",两种语义混用会打破单一快照的一致性。

理解混用边界的关键是认识到两种读各有一套一致性规则。工程上要么统一用快照读(读多写少),要么统一用当前读(需要严格串行读写),要避免在同一事务内混用导致语义混乱。这也是"先查后改"产生丢失更新和写偏斜的根源之一。

#
★★★

6. MySQL 可重复读下二级索引回表为何可能读到半新半旧的数据?MVCC 如何保证聚簇索引与二级索引的可见性一致?

在 MySQL 可重复读下,二级索引回表为何可能读到"半新半旧"的数据?MVCC 如何保证聚簇索引与二级索引的可见性一致?

  • 二级索引回表机制
  • MVCC 可见性判断
  • 索引列与隐藏列

二级索引回表是指通过二级索引定位到主键值,再回聚簇索引取完整行。在可重复读下,二级索引本身可能没有存储 trx_id 等隐藏列,其可见性判断需要回表到聚簇索引,用聚簇索引记录中的 trx_id 与 Read View 比较。MVCC 通过聚簇索引记录中的隐藏列(trx_id、roll_pointer)和 undo 版本链来保证可见性一致:无论从二级索引还是聚簇索引访问,最终都依据同一 Read View 和版本链判断可见版本,因此不会读到半新半旧的中间状态。若二级索引记录被更新,其保留的旧版本信息允许回表时找到正确的可见版本。

"半新半旧"的担忧来源于担心二级索引与聚簇索引的数据版本不同步。InnoDB 通过统一使用聚簇索引记录的版本信息进行可见性判断,保证两次回表看到的都是同一版本,从而维持一致性。理解索引结构对分析并发读现象很重要。

#
★★

7. 脏读场景为什么很少见,READ UNCOMMITTED 在主流数据库中的实现差异与适用边界?

为什么脏读场景在实际中很少见?READ UNCOMMITTED 在主流数据库中的实现差异与适用边界是什么?

  • 主流数据库对 READ UNCOMMITTED 的实现
  • 脏读少见的原因
  • 适用边界

脏读在主流数据库中很少见,主要原因有二:一是绝大多数业务默认使用 RC 或 RR,很少主动选择 RU;二是即便使用 RU,PostgreSQL 将其实现为等价于 READ COMMITTED,快照读不会读到未提交数据,因此不产生脏读;而 MySQL 的 InnoDB 在 RU 下普通 SELECT 直接读取记录的最新版本(不做可见性过滤),仍可能读到未提交数据、产生脏读。RU 的适用边界很窄:它主要用于某些对一致性要求极低、但对锁开销敏感的场景(如某些只读监控系统),而且在这些数据库里它并不能真正绕过 MVCC 的可见性约束。

要意识到"隔离级别是标准定义,实现是数据库各自选择"。PostgreSQL 将 RU 等价实现为 RC,不产生脏读;MySQL 的 InnoDB 在 RU 下则可能读到未提交的最新版本。脏读少见主要是因为业务很少选用 RU,其次是因为 PG 实际不产生脏读。适用边界应当结合"是否需要读取未提交数据"来决定,而绝大多数场景不需要。

#
★★

8. 脏读(Dirty Read)的定义,读取到其他事务未提交的数据?

什么是脏读(Dirty Read)?它的核心特征是什么?

  • 脏读的定义
  • 未提交数据的危害
  • 与不可重复读的区别

脏读指一个事务读取到另一个事务尚未提交的数据。如果该事务随后回滚,那么读取方基于的数据就从未真正存在过,这种数据被称为"脏数据"。脏读的危害在于读到的是可能被回滚的、不稳定的数据,会破坏数据的一致性。它必须发生在读得比写快、且写事务未提交时,因此在有 MVCC 的系统中较少出现。

脏读的核心是"读到未提交数据",与"读到已提交但已变化的数据"(不可重复读)有本质区别。理解脏读是理解隔离级别最低档 RU 的基础。

#
★★

9. 不可重复读的工程影响,同一事务内两次读不一致对统计、报表、缓存更新的影响与应对?

不可重复读对统计、报表、缓存更新等工程场景有哪些影响?应如何应对?

  • 不可重复读对业务的影响
  • 统计与报表的一致性
  • 应对策略

在同一事务内执行多次读取用于统计、报表或缓存更新时,如果发生不可重复读,会导致汇总数据前后不一致、报表出现矛盾、缓存更新基于过时或前后不一致的数据。应对方式有:使用 REPEATABLE READ 隔离级别使整个事务内读取一致;对需要稳定的读取加锁(SELECT ... FOR UPDATE / FOR SHARE)阻断并发写;或在应用层对关键读数进行快照缓存。对于统计类需求,RR 或显式锁能保证事务内聚合结果稳定。

不可重复读影响的本质是"读读一致性"被破坏。统计、报表这类需要多次读取汇总的场景对一致性敏感,因此应通过提升隔离级别或加锁来保证单次事务内读取结果稳定,避免在分布式或高并发下产生矛盾数据。

#
★★

10. MVCC 的实现要素,隐藏列(trx_id/roll_pointer)、undo log 版本链、Read View 的生成时机?

MVCC 的实现要素有哪些?隐藏列(trx_id/roll_pointer)、undo log 版本链、Read View 的生成时机分别是什么?

  • 隐藏列 trx_id 与 roll_pointer
  • undo log 版本链
  • Read View 生成时机

MVCC 的实现要素包括:每行记录上的隐藏列 trx_id(记录最近修改该行的事务 ID)和 roll_pointer(指向 undo 日志中该行旧版本的指针);undo log 版本链,通过 roll_pointer 把同一行的多个历史版本串联起来;Read View,记录当前事务发起读取时刻活跃事务的快照信息,用于判断某个版本是否可见。Read View 的生成时机取决于隔离级别:RC 下每条语句生成一次,RR 下事务内第一次读生成一次并复用。

这三个要素是 MVCC 的骨架。trx_id 和 roll_pointer 构成版本链,Read View 决定每个版本对当前事务是否可见,生成时机决定隔离级别差异。理解三者关系是掌握 MVCC 的关键。

#
★★

11. Read View 的可见性规则,trx_id 与活跃事务列表如何判断?

Read View 的可见性规则是什么?trx_id 与活跃事务列表如何配合判断某个版本是否可见?

  • Read View 的组成
  • 可见性判断规则
  • 活跃事务列表

Read View 主要包含:创建时尚未提交的最小事务 ID、已分配的最大事务 ID、以及创建时活跃事务(未提交)的 ID 列表。判断某版本记录(其 trx_id 为 T)是否可见,规则大致为:若 T 小于最小未提交事务 ID,说明 T 已提交,可见;若 T 大于最大已分配事务 ID,说明 T 是当前事务之后才启动的,不可见;若 T 在活跃事务列表中,说明 T 尚未提交,不可见;否则可见。当前事务自身写的数据始终可见。

可见性规则的核心是"该版本的事务是否已在 Read View 创建时提交"。通过最小/最大事务 ID 与活跃列表的组合,O(1) 或 O(活跃数) 地判断任意版本是否可见,这是 MVCC 高效实现的基础。

#
★★

12. 隔离级别与 MVCC 的实现,RC/RR 的快照生成时机?

RC 与 RR 隔离级别下,MVCC 的快照生成时机有何不同?

  • RC 的语句级快照
  • RR 的事务级快照
  • 生成时机对读一致性的影响

READ COMMITTED 下,每条 SELECT 语句执行时都生成一个新的 Read View(语句级快照),因此不同语句可能看到不同版本。REPEATABLE READ 下,事务内第一次读(SELECT)时生成 Read View 并复用整个事务(事务级快照),因此事务内所有快照读看到一致的数据。这是 RC 产生不可重复读而 RR 不产生(对快照读而言)的根本原因。

快照生成时机直接决定了隔离级别对读一致性的保证强度。RC 用"语句级快照"换取较低隔离代价,RR 用"事务级快照"换取读一致性。记住口诀"RC 每次语句新建 Read View、RR 事务首查新建"。

#
★★

13. PostgreSQL 默认 RC 与 MySQL 默认 RR 的差异及对业务的影响

PostgreSQL 默认 READ COMMITTED 与 MySQL 默认 REPEATABLE READ 的差异是什么?对业务有什么影响?

  • 默认隔离级别差异
  • 回滚与锁语义差异
  • 对业务的影响

PostgreSQL 默认 READ COMMITTED,MySQL(InnoDB)默认 REPEATABLE READ。差异体现在:RR 下同一事务内快照读结果一致,RC 下每条语句看到最新已提交数据;并发更新同一行时,两者都会让后执行者等前一个事务提交后再基于最新值继续,但 RR 下快照读与当前读的可见性不同,长事务持有快照,可能"读不到"其他事务刚提交的数据,而 RC 下每次读都能看到最新已提交数据。对业务影响:从 PG 迁移到 MySQL 时,长事务内可能"读不到"其他事务刚提交的数据(RR 快照),需注意;从 MySQL 迁移到 PG 时,事务内多次读可能结果不一致,需明确是否需要 RR。

默认隔离级别不同会在跨库迁移和并发语义上产生差异。理解默认级别有助于解读"为什么同样 SQL 在两个库行为不同",以及如何设置 isolation level 满足业务一致性需求。

#
★★

14. 不同隔离级别下"读到什么"的口诀,RC 每次语句新建 Read View、RR 事务首查新建、如何用实验验证?

不同隔离级别下"读到什么"的口诀是什么?如何用实验验证 RC 与 RR 的快照差异?

  • 口诀记忆
  • 实验验证方法
  • 快照生成时机

口诀是:RC 每次语句新建 Read View,RR 事务首查新建并复用。验证实验:开两个事务,事务 A 先 SELECT 一次,事务 B 更新并提交同一行,事务 A 再 SELECT 一次;在 RC 下第二次 SELECT 能看到 B 提交的新值,在 RR 下第二次 SELECT 仍看到旧值。这能直观验证 RC 与 RR 的快照差异。

用"两事务 + 两次读"的经典实验即可验证快照时机。理解口诀有助于快速判断不同隔离级别下读到的数据版本,是面试中常考的实现细节。

#
★★

15. MySQL 默认 autocommit=1 时每条语句都是独立事务,长连接中未显式提交的事务会让 Read View 长期保留,对快照与 undo 清理有何影响?

MySQL 默认 autocommit=1 时每条语句都是独立事务,那么长连接中未显式提交的事务会让 Read View 长期保留,对快照与 undo 清理有何影响?

  • autocommit 的影响
  • 长事务与 Read View 保留
  • undo log 清理

autocommit=1 时每条语句自动提交,独立成事务,这通常不会积累 undo。但长连接中如果显式开启事务(BEGIN)后迟迟不提交,该事务的 Read View 会长期保留,导致 undo 日志中此前版本无法被 purge 线程清理,因为 Read View 认为这些版本仍可能被读取。结果是 undo 日志膨胀、版本链变长,影响性能并可能耗尽 undo 空间。

Read View 的保留限制了 undo 的清理边界:purge 只能清理"比所有活跃 Read View 更旧"的版本。长事务等于把 Read View 生命周期拉长,从而阻塞清理。这是长事务危害的重要来源。

#
★★

16. 长事务或长查询为什么会导致 undo 日志膨胀与版本链变长?purge 线程的清理边界是什么?

长事务或长查询为什么会导致 undo 日志膨胀与版本链变长?purge 线程的清理边界是什么?

  • undo 膨胀机制
  • 版本链变长
  • purge 清理边界

长事务或长查询会长期持有 Read View,使该事务认为大量旧版本仍可见,因此这些旧版本不能被清理,undo 日志不断累积,版本链越拉越长。purge 线程的清理边界是:只能清理所有权已经小于所有活跃事务 Read View 的最小可以看到的版本(即删除那些不再被任何活跃 Read View 需要的旧版本),若存在长事务,其 Read View 会把清理边界"卡住",导致旧版本无法回收。

undo 的清理取决于"是否有活跃事务仍能看到旧版本"。长事务正是通过延长 Read View 生命周期来阻止清理。这是数据库运维中控制长事务数量和时长的重要原因。

#

17. RC 下两次 SELECT 之间其他事务提交如何改变结果集?如何用 REPEATABLE READ 或加锁读稳定结果?

在 RC 下,两次 SELECT 之间其他事务提交会如何改变结果集?如何用 REPEATABLE READ 或加锁读稳定结果?

  • RC 下结果集变化
  • 稳定结果的方案
  • RR 与加锁读

RC 下每次 SELECT 都使用新快照,若两次 SELECT 之间其他事务提交了对相关行的修改或插入/删除,第二次 SELECT 的结果集就会变化(行值变化,甚至行集合变化)。要稳定结果,可改用 REPEATABLE READ 使事务内快照读一致,或对读取加锁(SELECT ... FOR UPDATE / FOR SHARE)阻止并发修改,从而保证两次读取结果一致。

结果集不稳定源于 RC 的语句级快照。RR 用事务级快照解决,加锁读用阻塞并发写解决。两种方案各有取舍:RR 提高读一致性、代价是更长的锁/快照,加锁读会降低并发。

#

18. 事务隔离级别在并发编程中的类比,与 Java 内存模型?

事务隔离级别在并发编程中与 Java 内存模型(JMM)有什么类比?

  • 隔离级别与 JMM 类比
  • 可见性保证
  • 易变性与有序性

事务隔离级别与 Java 内存模型类似,都涉及"可见性"与"一致性"问题。JMM 通过 volatile、synchronized 等机制保证多线程对共享变量的可见性与有序性;数据库通过隔离级别与锁保证多事务对数据的可见性与一致性。RC 类似"稍弱的内存可见性"(能看到最新已提交),RR 类似"事务内稳定的可见性快照",SERIALIZABLE 类似"全同步的强一致"。两者都把"并发安全"建立在"可见性 + 约束"之上。

类比的意义在于理解"一致性难题"是通用的。JMM 的 volatile 保证可见性、数据库的快照保证读一致性,核心思想相似。这类题目考察对并发抽象的理解,而非具体实现。

#

19. 基于表锁的存储引擎(如 MyISAM)读写互斥,为什么天然不存在脏读与不可重复读?

基于表锁的存储引擎(如 MyISAM)读写互斥,为什么天然不存在脏读与不可重复读?

  • 表锁互斥
  • 读写串行
  • 与 MVCC 的对比

MyISAM 使用表级锁,读写操作互斥:写操作会独占整张表,读操作也需获取读锁,读与写不能同时进行。由于同一时刻表上只有一个写者,且写者持锁期间其他读被阻塞,读操作只能看到已完整提交的数据,因此不存在"读到未提交数据"(脏读);又因为读期间写被阻塞,同一事务内两次读之间不会有其他事务提交的修改,因此也不存在不可重复读。这是以牺牲并发度为代价换来的强一致性。

表锁通过"完全串行化访问"从根本上消除了脏读与不可重复读,代价是极低的并发度。这与 MVCC 的"读写不互斥、靠版本解决"形成鲜明对比,是理解锁粒度与并发度权衡的经典案例。