丢失更新与死锁等待图

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

1. 丢失更新的防止,SELECT FOR UPDATE、版本号、CAS、SSI?

丢失更新可以通过哪些方式防止?SELECT FOR UPDATE、版本号、CAS、SSI 分别如何起作用?

  • 悲观锁(FOR UPDATE)
  • 乐观锁(版本号/CAS)
  • SSI 检测

丢失更新可通过多种方式防止:SELECT ... FOR UPDATE 是悲观锁,把读操作变成当前读并加锁,使"读-改-写"串行化,防止并发覆盖;版本号(乐观锁)每次更新时校验版本号,若版本已变化则中止重试,保证"读-改-写"基于同一版本;CAS(Compare-And-Swap)用条件更新(UPDATE ... WHERE 期望值)原子地判断后写入;SSI 通过检测读写依赖环,在提交时中止可能造成丢失更新的冲突事务。它们分别从"加锁串行""版本校验""原子条件""依赖检测"四个角度防丢失更新。

丢失更新源于"先读后写、无锁",四个手段本质都是让"读-改-写"原子化或冲突可检测。悲观锁牺牲并发,乐观锁增加重试,SSI 由数据库检测。选择取决于冲突率与并发要求。

#
★★★

2. 丢失更新(Lost Update)的定义,两个事务读-改-写同一行,后写覆盖先写?

什么是丢失更新(Lost Update)?为什么两个事务读-改-写同一行时,后写会覆盖先写?

  • 丢失更新的定义
  • 读-改-写竞态
  • 后写覆盖先写

丢失更新指两个事务同时读取同一行,各自基于读到的旧值做修改,再写回,后写者覆盖了先写者的修改,导致先写者的更新"丢失"。其本质是"读-改-写"不是原子的:两个事务都读到旧值,都把基于旧值计算的结果写回,最终只有后写者的结果保留。由于两个事务都先读后写且无锁,各自的写都覆盖了对方,从而丢失了更新。

丢失更新是"先读后写"竞态的典型。关键点:两个事务都读到相同的旧值,都基于旧值计算,若没有锁或版本控制,后写者会覆盖先写者。理解这一点是掌握解决手段(加锁/版本/CAS)的基础。

#
★★★

3. 应用层乐观锁(Optimistic Locking)的实现,版本号比较?

应用层乐观锁(Optimistic Locking)是如何通过版本号比较实现的?

  • 乐观锁原理
  • 版本号字段
  • 更新时校验

乐观锁通过一个版本号字段(如 version)实现。更新时先读取记录的版本号,修改后在 UPDATE 语句的 WHERE 条件中带上版本号,例如 UPDATE t SET value=?, version=version+1 WHERE id=? AND version=?。若影响行数为 1,说明版本未变,更新成功;若影响行数为 0,说明版本已变化(被其他事务修改),则更新失败,需要重新读取并重试。整个过程不持锁,靠"更新时校验版本"保证并发安全。

乐观锁的核心是把"读-改-写"的冲突检测推迟到写操作:通过条件更新 + 版本号让"冲突"以"影响行数为 0"的形式暴露。它不阻塞读,适合并发冲突率较低的读多写少场景。

#
★★★

4. REPEATABLE READ 是否完全防止丢失更新?

REPEATABLE READ 是否完全防止丢失更新?为什么?

  • RR 的读一致性
  • 快照读与当前读
  • 丢失更新的发生

REPEATABLE READ 并不完全防止丢失更新。RR 只保证快照读(普通 SELECT)在事务内读一致,但"先 SELECT 判断、再 UPDATE/INSERT"的读-改-写流程中,若使用普通 SELECT(快照读)获取旧值,再 UPDATE(当前读)写回,两个事务仍可能都读到旧值并各自写回,后写覆盖先写,从而丢失更新。只有把读取也变成当前读(SELECT ... FOR UPDATE)加锁,或用带版本号/条件更新的原子写,才能防止丢失更新。RR 防止的是不可重复读和幻读,不保证读-改-写原子性。

关键区分:RR 的快照读保证"读一致",但"读-改-写"需要原子性。RR 下若先快照读后写,判断基于旧快照,写入基于最新数据,仍可能丢失更新。因此需要通过加锁或版本号来把读-改-写原子化。

#
★★★

5. InnoDB 死锁检测的触发与代价(等待图、victim 选择、innodb_deadlock_detect 权衡)

InnoDB 死锁检测的触发机制与代价是什么?等待图、victim 选择、innodb_deadlock_detect 的权衡分别是什么?

  • 死锁检测机制
  • 等待图
  • victim 选择

InnoDB 通过维护锁等待图(Wait-for Graph)来检测死锁:每个事务是节点,事务之间的锁等待构成边,当图出现环时即判定死锁。检测到死锁后,InnoDB 选择回滚一个事务(victim),通常选择回滚代价最小(数据修改量最少、被回滚的事务工作量最小)的事务,并返回错误码 1213。死锁检测的代价是:每次锁等待都会遍历等待图,在高并发下开销大,因此提供 innodb_deadlock_detect 参数,可在确认死锁极少时关闭检测,改用 innodb_lock_wait_timeout 超时兜底。权衡在于:开启检测及时回滚但增加检测开销,关闭检测减少开销但死锁只能靠超时较长地暴露。

死锁检测是"环检测 + 选择牺牲者"。代价是等待图遍历的 CPU 开销。当并发极低、死锁几乎不发生时,可关闭检测以省开销,靠超时兜底;但超时等待时间较长,影响体验。理解权衡有利于在高并发场景调优。

#
★★★

6. 两个并发 UPDATE 会因行锁串行化而互见结果,为什么先 SELECT 再 UPDATE 的读-改-写才是真正的丢失更新?请说明两者的本质区别。

两个并发 UPDATE 会因行锁串行化而互见结果,为什么先 SELECT 再 UPDATE 的读-改-写才是真正的丢失更新?两者的本质区别是什么?

  • 并发 UPDATE 的串行化
  • 读-改-写竞态
  • 本质区别

两个并发 UPDATE 语句直接作用于同一行时,由于 UPDATE 是当前读,会先加行锁,后执行者会等待前一个 UPDATE 提交后才执行,并基于前一个事务提交后的最新值计算,因此不会覆盖,不会丢失更新。而"先 SELECT(快照读)再 UPDATE"的读-改-写流程中,SELECT 不加锁,两个事务可能都读到相同的旧值,然后各自 UPDATE 写回,后写者覆盖先写者,造成丢失更新。本质区别在于:UPDATE 本身是原子的当前读(加锁、读最新值),而 SELECT+UPDATE 的"读-改-写"不是原子的,判断与写入之间没有锁保护,存在竞态窗口。

核心是"原子性":UPDATE 语句内部是原子的(读最新 + 加锁 + 写),而"先 SELECT 再 UPDATE"把读和写拆开,中间留出竞态窗口。因此真正丢失更新发生在"读-改-写"流程,而非两条 UPDATE 之间。

#
★★

7. PostgreSQL 与 MySQL 死锁检测的实现差异?

PostgreSQL 与 MySQL 在死锁检测的实现上有何差异?

  • MySQL 死锁检测
  • PostgreSQL 死锁检测
  • 检测时机与机制

两者都通过等待图检测死锁,但实现细节不同。MySQL InnoDB 由后台死锁检测线程周期性地或在必要时构建等待图检测环,一旦发现死锁立即回滚一个牺牲事务(通常回滚代价最小者),并返回 1213 错误。PostgreSQL 在事务等待锁时,通过"等待者通知"机制(deadlock detection)追踪等待链,当检测到环时中止其中一个事务,返回错误码 40P01。PostgreSQL 的检测更侧重在等待时局部构建等待图,MySQL 则维护全局锁等待图。两者核心都是"检测环 + 中止牺牲者"。

死锁检测的共性(等待图 + 环检测 + 选择牺牲者)是重点,差异在检测时机与调度方式。面试常考错误码(MySQL 1213 / PG 40P01)与参数(MySQL 的 innodb_deadlock_detect)。

#
★★

8. 等待图(Wait-for Graph)的概念,节点是事务,边是等待关系?

等待图(Wait-for Graph)的概念是什么?节点与边分别代表什么?

  • 等待图定义
  • 节点与边
  • 环与死锁

等待图(Wait-for Graph)是检测死锁的核心数据结构:节点代表事务,有向边代表等待关系(如事务 A 等待事务 B 持有的锁,则存在 A→B 的边)。若图中存在环(如 A→B→A),说明存在循环等待,即死锁。数据库在检测到等待图出现环时,会中止其中一个事务(victim)来解除死锁。等待图是死锁检测的基础。

等待图把"事务间锁等待"建模成有向图,"环"就是死锁发生的充要条件。理解"节点=事务、边=等待、环=死锁"是分析死锁与死锁检测的关键。

#
★★

9. 等待图环检测算法,DFS 或 BFS 检测环?

等待图的环检测算法是什么?如何用 DFS 或 BFS 检测环?

  • 环检测算法
  • DFS 与 BFS
  • 复杂度

等待图的环检测可用深度优先搜索(DFS)或广度优先搜索(BFS)实现。DFS 检测环是最常用的方法:从某个节点出发递归遍历,若在遍历过程中遇到"当前遍历路径上的节点(即递归栈中)"再次被访问,则说明存在环(死锁)。BFS 也可通过拓扑排序检测:对图做拓扑排序,若无法排完所有节点则说明存在环。死锁检测通常用 DFS 因其实现简单,且能在检测到环时准确定位环上的事务。复杂度为 O(V+E),V 为事务数,E 为等待边数。

环检测是图论经典问题。DFS 用"递归栈标记"判断环,BFS/拓扑排序用"能否全排完"判断环。死锁检测需要用环上的事务信息来选择牺牲者,DFS 更便于定位。

#
★★

10. “先读后写”的丢失更新竞态,SELECT 判断 + UPDATE 之间如何被并发破坏?原子条件更新(UPDATE ... WHERE 状态=期望值)为何是首选修复?

"先读后写"的丢失更新竞态是如何被并发破坏的?为什么原子条件更新(UPDATE ... WHERE 状态=期望值)是首选修复?

  • 先读后写竞态
  • 条件更新
  • 原子性

"先读后写"中,SELECT 判断状态后,在 UPDATE 之前有一个竞态窗口:若此时其他事务提交了修改,当前事务的 UPDATE 会基于过期的判断覆盖新状态,导致丢失更新。原子条件更新(UPDATE ... WHERE 状态=期望值)把"判断 + 写入"合并为一条原子 SQL,只需判断影响行数即可知道是否成功:若影响行数为 1 说明状态正是期望值,更新成功;若为 0 说明状态已变化,需重试或报错。它是首选修复,因为一条语句由数据库原子执行,无竞态窗口,且无需显式加锁,适合简单状态流转。

条件更新是"把检查下沉到写入"的经典手法,用一条原子语句消除"读-改-写"之间的竞态窗口。相比加锁,它更轻量且不阻塞读;相比版本号,它直接用业务状态做条件。它是防丢失更新的首选方案。

-- 原子条件更新:仅当状态仍为预期值时更新
UPDATE account SET balance = balance - 100, status = 'paid'
WHERE id = 1 AND status = 'unpaid';
-- 若影响行数为 0,说明状态已变化,需重试或报错
#
★★

11. 从错误码与日志区分死锁(MySQL 1213 / PG 40P01)与锁等待超时(MySQL 1205),各自的重试与降级策略有何不同?

如何从错误码与日志区分死锁(MySQL 1213 / PG 40P01)与锁等待超时(MySQL 1205)?各自的重试与降级策略有何不同?

  • 错误码区分
  • 死锁与锁等待超时
  • 重试与降级策略

MySQL 死锁返回错误码 1213(ER_LOCK_DEADLOCK),PostgreSQL 死锁返回 40P01(deadlock_detected);MySQL 锁等待超时返回 1205(ER_LOCK_WAIT_TIMEOUT),由 innodb_lock_wait_timeout 控制。死锁是"循环等待,检测后立即回滚一个事务",锁等待超时是"单方面等待超过设定时间后放弃"。策略上:死锁通常可立即重试整个事务(因为已回滚,需重新执行);锁等待超时也可能回滚,需判断回滚范围后重试。重试前应视情况重新读取最新数据,避免沿用过期数据。高并发下可引入退避与限流,避免重试风暴。

区分错误码有助于定位问题类型。死锁是系统检测到环后主动回滚立即返回,锁等待超时是一个事务等待另一个锁超过阈值。重试策略都要考虑幂等性与重试上限,避免无限重试放大压力。

#
★★

12. 丢失更新的发生条件,先读后写且无锁,两个事务互相覆盖?

丢失更新的发生条件是什么?为什么"先读后写且无锁"会导致两个事务互相覆盖?

  • 发生条件
  • 先读后写无锁
  • 互相覆盖

丢失更新的发生条件是"先读后写且无锁":两个事务先各自读取同一行(无锁或快照读),再基于各自读到的旧值修改并写回。由于两个事务都读到相同的旧值,后写者覆盖先写者,使先写者的更新丢失。若"读-改-写"被加锁(当前读 FOR UPDATE)或原子化(条件更新/版本号),两个事务会串行执行或检测到冲突,就不会互相覆盖。因此无锁的"先读后写"是丢失更新的充分条件。

丢失更新三要素:读旧值、基于旧值计算、写回,且中间无锁。任何破坏"无锁"或"基于旧值"的手段(加锁、版本、条件更新)都能防丢失更新。

#
★★

13. 乐观锁与悲观锁解决丢失更新,版本号与 FOR UPDATE?

乐观锁与悲观锁分别如何解决丢失更新?版本号与 FOR UPDATE 的作用是什么?

  • 乐观锁(版本号)
  • 悲观锁(FOR UPDATE)
  • 各自的适用场景

悲观锁用 SELECT ... FOR UPDATE 把相关行加锁,使"读-改-写"串行化,后执行者等待前一个事务提交,不会覆盖;乐观锁用版本号字段,更新时校验版本号,若版本已变化则中止重试。悲观锁适合冲突率高、写频繁的场景,能减少重试但降低并发;乐观锁适合冲突率低、读多写少的场景,不阻塞读但冲突时需要重试(可能产生重试风暴)。两者都通过"让读-改-写安全"来防丢失更新。

悲观锁"先锁后操作",乐观锁"先操作后校验"。选择依据是冲突率与并发要求。理解两者权衡是掌握并发控制的关键。

#
★★

14. 死锁(立即回滚一个事务)与锁等待超时(innodb_lock_wait_timeout)的区别与处理

死锁(立即回滚一个事务)与锁等待超时(innodb_lock_wait_timeout)的区别是什么?如何处理?

  • 死锁与锁等待超时区别
  • innodb_lock_wait_timeout
  • 处理方式

死锁是系统检测到等待图出现环后,立即选择回滚一个事务(victim)来解除循环,返回 1213;锁等待超时是单个事务等待某把锁超过 innodb_lock_wait_timeout(默认 50 秒)后放弃,返回 1205。死锁是"系统主动解除",等待超时是"等待方被动放弃"。处理上:死锁可立即重试整个事务;锁等待超时需判断是否回滚,若回滚则重试,并考虑调整超时时间或优化锁竞争。两者都需避免无限制重试,应配合重试上限与退避。

关键区别是"解除机制":死锁由数据库检测环后主动回滚,等待超时由等待方超时放弃。理解差异有助于制定重试与降级策略,并调优 innodb_lock_wait_timeout

#
★★

15. 乐观锁冲突重试的设计(重试次数、退避、冲突率监控)

乐观锁冲突重试的设计要点是什么?重试次数、退避、冲突率监控分别如何考虑?

  • 重试次数
  • 退避策略
  • 冲突率监控

乐观锁冲突重试应设计:一是重试次数上限,避免无限重试导致重试风暴和长尾延迟,通常设 3~5 次;二是退避策略,重试失败后指数退避或随机抖动,避免多个线程同时重试再次冲突;三是冲突率监控,统计乐观锁冲突/重试比例,若冲突率过高(说明并发写多),说明乐观锁不适用,应改用悲观锁或队列化。此外重试前应重新读取最新数据(含最新版本号),避免基于过期数据反复失败。

乐观锁重试是一场"与并发冲突的博弈",重试次数、退避、冲突率监控三者共同保证系统在冲突下的稳定与可观测。冲突率是判断乐观锁是否适用的重要指标。

#
★★

16. 活锁(livelock)与锁饥饿和死锁有何区别?数据库如何通过锁队列公平性避免某个事务永远拿不到锁?

活锁(livelock)与锁饥饿、死锁有何区别?数据库如何通过锁队列公平性避免某个事务永远拿不到锁?

  • 活锁与锁饥饿定义
  • 与死锁的区别
  • 锁队列公平性

死锁是"循环等待、互相阻塞、都拿不到锁",活锁是"事务不断重试但总是被其他事务抢先,永远无法推进"(如不断重试加锁但总被更高优先级抢走),锁饥饿是"某事务因优先级低长期得不到锁,但系统本身在推进"。死锁是"谁都进行不下去",活锁/饥饿是"有事务在推进但某方永远等不到"。数据库通过锁队列的公平性(如 FIFO 先来先服务、按请求顺序排队)保证等待者按顺序获得锁,避免后到者不断插队导致先到者饿死。MySQL/PG 的锁管理通常按等待顺序授予,避免饥饿。

三者区别于"受阻的性质":死锁是环,活锁是重试但总被抢先,饥饿是长期等不到。锁队列公平性(FIFO)是防止饥饿/活锁的关键手段。

#
★★

17. 如何用 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段定位死锁事务与语句?等待图与回滚选择如何体现在日志中?

如何用 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段定位死锁事务与语句?等待图与回滚选择如何体现在日志中?

  • SHOW ENGINE INNODB STATUS
  • LATEST DETECTED DEADLOCK 段
  • 死锁定位与回滚选择

执行 SHOW ENGINE INNODB STATUS 后,LATEST DETECTED DEADLOCK 段会列出最近一次死锁的信息:包括两个(或多个)事务的 ID、持有/等待的锁(记录锁、间隙锁、临键锁的具体行)、等待关系、以及产生死锁的 SQL 语句。通过这些信息可以构建等待图,定位互相等待的锁与语句。日志还显示谁被选择为 victim(回滚者),通常是"回滚代价最小的事务"(修改行数少的事务)。定位后需分析两条 SQL 的加锁顺序,调整加锁顺序或缩短事务来避免死锁。

LATEST DETECTED DEADLOCK 是 MySQL 定位死锁的利器。它把死锁的等待图、锁等待细节、SQL 语句和 victim 选择都记录出来,是排查死锁的第一手资料。理解如何读取该段有助于快速定位并修复死锁。

#

18. 死锁避免的工程清单,统一加锁顺序、缩短事务、索引优化与并发度控制?

死锁避免的工程清单有哪些?统一加锁顺序、缩短事务、索引优化与并发度控制分别如何起作用?

  • 统一加锁顺序
  • 缩短事务
  • 索引优化与并发度控制

死锁避免的工程清单包括:统一加锁顺序,让所有事务按相同顺序访问资源(如按主键顺序),避免交叉加锁形成环;缩短事务,减少事务内持锁时间与所持锁数量,降低环形成的概率;索引优化,让 UPDATE/DELETE 走索引缩小加锁范围,避免全表扫描加大量锁;并发度控制,限制同一资源的并发操作数量(队列化、限流),减少锁冲突。这些措施从"减少环的形成"角度系统性降低死锁。

死锁是"环",避免死锁的核心是"让环难以形成":统一顺序消除交叉,缩短事务减少持锁重叠,索引优化减少锁范围,控并发减少冲突。工程上通常组合使用。

#

19. 死锁回滚后的事务重试如何保证幂等?重试前需要重新读取最新数据还是直接重放原 SQL?

死锁回滚后的事务重试如何保证幂等?重试前需要重新读取最新数据还是直接重放原 SQL?

  • 幂等性
  • 重试前重新读取
  • 重放原 SQL 的风险

死锁回滚后重试,需保证幂等:重试前应重新读取最新数据(而不是直接重放基于旧快照的原 SQL),因为回滚后之前读到的数据可能已过期,直接重放会基于过期数据再次冲突或产生错误结果。同时,若业务有"插入/扣减"类操作,需用幂等键或唯一约束保证重复执行不产生重复数据。重试还应有限次数上限,配合退避。对于纯依赖当前数据的 UPDATE,重放原 SQL 可能可行,但更稳妥的是重新读取最新值再构造新的语句。

重试的关键是"基于最新数据重做"而非"重放旧逻辑"。回滚后旧快照失效,直接重放会沿用过期数据。幂等性(幂等键、唯一约束)保证即使发生重复执行也不产生脏数据。这二者共同保证重试安全。