谓词锁、死锁与乐观并发控制

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

1. InnoDB 中 Next-Key Lock 的索引锁实现?

InnoDB 中 Next-Key Lock 是如何基于索引实现的?

  • 临键锁基于索引
  • 索引记录与间隙
  • 无索引时退化为全表锁

InnoDB 的 Next-Key Lock 是基于索引实现的:锁加在索引记录及其间隙上,而非数据行本身。由于 InnoDB 所有查询都通过索引(聚簇索引或二级索引)定位记录,临键锁自然作用在索引记录上,锁定的区间由索引键值决定。若查询使用了二级索引,则同时在二级索引记录和聚簇索引记录上加锁。若查询没有走索引(全表扫描),InnoDB 会对表的每一条记录加临键锁,实际等同于锁住整个表,锁冲突极大。因此,临键锁"锁索引"的机制决定了合理索引对于控制锁范围至关重要。

临键锁"锁索引"的本质,决定了加锁范围与索引的选择强相关。没有合适索引时,临键锁会退化到全表加锁,造成严重锁竞争。

#
★★★

2. PostgreSQL SSI 中的谓词锁实现?

PostgreSQL 的可串行化快照隔离(SSI)中谓词锁是如何实现的?

  • SSI 与谓词锁
  • SIREAD 锁
  • 检测写偏斜

PostgreSQL 的 SERIALIZABLE 隔离级别通过可串行化快照隔离(SSI)实现,它使用谓词锁(predicate lock)记录事务读取的数据范围。SSI 对每个读操作维护 SIREAD 锁,记录读了哪些谓词范围(元组、页、表级),并在写操作时检测与已读数据的读写反依赖(rw-antidependency)。当检测到两个事务以不同顺序读写同一数据,可能形成串行化异常(如写偏斜)时,SSI 会终止其中一个事务让其重试。谓词锁用于跟踪"读过的条件范围",使 SSI 能判断是否存在会破坏串行化的冲突。

SSI 的谓词锁是"记录读范围"的机制,配合读写反依赖检测,实现比 MVCC 快照隔离更强的串行化保证。它比传统锁粒度更粗(可合并),以降低开销。

#
★★★

3. 索引锁(Index Lock),锁定索引项而非数据行?

索引锁(Index Lock)是什么?它如何锁定索引项而非数据行?

  • 索引锁的概念
  • 锁定索引项
  • 与数据行锁的关系

索引锁(Index Lock)是锁定索引项(index entry)的锁,而不是直接锁定数据行。在 InnoDB 中,索引锁与行锁是统一的:由于聚簇索引的叶子节点就是数据行,行锁实际就是加在聚簇索引项上的锁;对二级索引记录的锁则加在二级索引项上,并通过回表加锁聚簇索引项。PostgreSQL 中也有 predicate 锁作用在索引页或索引项上。索引锁的粒度决定了锁冲突范围:索引选择合理,锁就精确;索引不生效,锁范围就扩大。索引锁是行锁与索引的实现载体。

"锁在索引上"是 InnoDB 行锁的本质。理解索引锁与数据行的关系,能解释为什么 InnoDB 必须有索引、以及索引如何影响锁范围。

#
★★★

4. 谓词锁(Predicate Lock)的概念,锁定满足谓词的元组集合?

谓词锁(Predicate Lock)的概念是什么?它如何锁定满足谓词的元组集合?

  • 谓词锁的概念
  • 锁定满足谓词的元组集合
  • 粒度合并

谓词锁(Predicate Lock)是锁定"满足某查询谓词的所有元组集合"的锁,而非单个元组。例如 SELECT ... WHERE age > 30 会锁定所有年龄大于 30 的元组(及其未来可能插入的元组)。它用于防止幻读和串行化异常:若另一个事务插入满足谓词的新元组或修改违背谓词的元组,就会与谓词锁冲突。PostgreSQL SSI 使用谓词锁来跟踪读范围。由于谓词可能命中大量元组,谓词锁支持粒度合并(元组→页→表),从粗粒度到细粒度,降低锁开销。

谓词锁是"按条件锁定范围"的锁,比按行锁更抽象,能覆盖未插入的元组。它通过粒度合并(coarsening)控制开销,是 SSI 检测串行化异常的基础。

#
★★★

5. 锁粒度(Tuple、Page、Relation)的合并策略?

锁粒度(Tuple、Page、Relation)的合并策略是什么?为什么需要合并?

  • 锁粒度层级
  • 粒度合并策略
  • 合并的权衡

锁粒度从细到粗有 Tuple(元组)、Page(页)、Relation(表)。粒度合并(coarsening)策略是:当一个事务锁定的元组数量过多或原因复杂时,把元组级锁合并为页级锁,甚至表级锁,以减少锁的数量和内存开销。PostgreSQL SSI 的谓词锁就采用这种合并:把多个元组谓词锁合并到页级,再合并到表级。合并的好处是降低锁管理开销、提升性能;代价是扩大锁冲突范围,可能误伤本可并行的操作(误报串行化异常)。因此合并策略在"锁开销"与"并发精度"之间权衡。

锁粒度合并是"以并发精度换锁开销"的优化。理解合并策略,能解释为什么 SSI 在元组过多时会误报,以及为什么需要权衡粒度。

#
★★★

6. 死锁的检测,PostgreSQL 与 MySQL 的等待图算法?

死锁的检测算法是什么?PostgreSQL 与 MySQL 的等待图算法有何异同?

  • 等待图算法
  • PostgreSQL 与 MySQL 的检测
  • 回滚策略

死锁检测通常使用等待图(wait-for graph)算法:把事务作为节点,如果事务 A 等待事务 B 持有的锁,则画一条从 A 到 B 的边;当图中出现环时,说明存在死锁。PostgreSQL 在后台周期检测(deadlock_timeout)等待图,发现环后回滚一个事务(基于代价)。MySQL InnoDB 也使用等待图检测,发现环时回滚代价较小的事务(依据 undo 大小等)。两者都是"检测环 + 回滚一方"的模式,区别在于检测周期与回滚对象的选择策略。等待图算法能精确识别死锁,但检测本身有开销。

等待图是死锁检测的通用算法,核心是"找环"。理解检测机制能帮助分析死锁日志、预测死锁场景,并设计规避策略。

#
★★★

7. 死锁(Deadlock)的产生条件,互斥、占有并等待、非抢占、循环等待?

死锁(Deadlock)的产生条件是什么?为什么需要互斥、占有并等待、非抢占、循环等待四个条件?

  • 死锁四个必要条件
  • 互斥、占有并等待、非抢占、循环等待
  • 破坏条件预防死锁

死锁的产生需要同时满足四个必要条件:互斥(Mutual Exclusion,资源只能被一个事务独占)、占有并等待(Hold and Wait,事务已持有资源又在等待其他资源)、非抢占(No Preemption,已持有的资源不能被强行抢占)、循环等待(Circular Wait,存在一个等待环)。破坏其中任意一个条件即可预防死锁。例如:让所有事务按一致顺序加锁(破坏循环等待)、一次性申请所有资源(破坏占有并等待)、超时抢占(破坏非抢占)。数据库通常靠检测回滚而非预防,但理解四条件有助于设计规避策略。

四条件是死锁的充分必要条件,也是死锁预防的切入点。理解它们能指导加锁顺序设计、资源申请策略等,减少死锁概率。

#
★★★

8. PostgreSQL SSI 如何用 SIREAD 锁与读写反依赖(rw-antidependency)检测串行化异常(如写偏斜),检测到后如何终止事务?

PostgreSQL SSI 如何用 SIREAD 锁与读写反依赖检测串行化异常(如写偏斜)?检测到后如何终止事务?

  • SIREAD 锁
  • 读写反依赖
  • 写偏斜检测与终止

SSI 中,每个读操作都会获取 SIREAD 锁,记录读过的谓词范围。当两个事务并发执行且构成"读写反依赖"(rw-antidependency)时——即事务 A 读了某数据,事务 B 写了该数据后提交,而 A 又基于旧读结果写数据,形成与串行化不一致的依赖——SSI 检测到这种冲突会判定串行化异常。写偏斜(write skew)就是典型例子:两个事务都基于旧快照读同一组数据,然后各自修改不同行,导致最终状态与任何串行执行不一致。检测到异常后,SSI 会终止其中一个事务(返回"could not serialize access due to read/write dependencies"错误),让应用重试。

SSI 通过 SIREAD 锁跟踪读范围、通过读写反依赖检测串行化异常,比普通快照隔离更强。检测到后终止一方,应用需重试。理解写偏斜避免了"两事务互相覆盖"的乐观陷阱。

#
★★

9. 锁等待队列(Lock Wait Queue)的实现,FIFO?

锁等待队列(Lock Wait Queue)是如何实现的?是否是 FIFO?

  • 锁等待队列
  • FIFO 公平性
  • 锁请求排队

锁等待队列(Lock Wait Queue)是数据库为等待同一把锁的事务维护的队列。当一个事务无法立即获取锁时,会被加入等待队列,待锁释放后依次获取。实现上,数据库通常采用先进先出(FIFO)来保证公平性,避免"饥饿"(后到的事务一直抢占)。但并非严格 FIFO,部分数据库在锁释放时会根据优先级、事务类型或优化调整唤醒顺序。InnoDB 与 PostgreSQL 大致按请求顺序排队,保证公平性。FIFO 消除了饥饿,但也可能让"先到者"因等待而阻塞后续无关事务。

锁等待队列的公平性(FIFO)防止饥饿,是锁机制的基本保障。理解排队机制有助于分析锁等待时间与锁冲突的连锁效应。

#
★★

10. MySQL 中 OCC 的应用,SELECT ... FOR UPDATE 与版本号?

MySQL 中乐观并发控制(OCC)如何应用?SELECT ... FOR UPDATE 与版本号机制是什么?

  • OCC 的概念
  • SELECT ... FOR UPDATE 悲观锁
  • 版本号乐观锁

MySQL 中,OCC 通常通过"版本号"实现:表中增加一个 version 列,读取时记录 version,更新时用 WHERE version = 旧版本 条件,若更新影响行数为 0 说明版本已被他人修改,需重试。这是乐观并发控制(提交时验证冲突)。而 SELECT ... FOR UPDATE 是悲观锁:读取时直接加锁,防止其他事务并发修改,属于悲观并发控制。两者取舍:FOR UPDATE 会阻塞并发、适合冲突率高的场景;版本号乐观锁不加锁、适合冲突率低的场景,但会浪费重试。实际应用中,库存扣减等场景常用版本号或条件更新实现乐观控制。

OCC(版本号)与悲观锁(FOR UPDATE)是两种并发控制范式。版本号在更新时验证冲突,适合读多写少;FOR UPDATE 提前加锁,适合写冲突多。理解二者适用场景是关键。

#
★★

11. PostgreSQL 的 SERIALIZABLE 与 OCC 的关系?

PostgreSQL 的 SERIALIZABLE 与乐观并发控制(OCC)是什么关系?

  • SERIALIZABLE 与 SSI
  • OCC 的推迟检测
  • 两者对比

PostgreSQL 的 SERIALIZABLE 隔离级别通过 SSI(可串行化快照隔离)实现,它本质上是一种乐观并发控制:事务在快照下自由执行,不加锁,提交时检测串行化异常(读写反依赖),发现冲突则终止一方。这与 OCC"假设无冲突、提交时验证"的思想一致。区别在于:SSI 使用谓词锁(SIREAD)跟踪读范围,检测的是"能否串行化"的冲突(写偏斜等),比普通 OCC 的版本号验证更强。因此 PostgreSQL 的 SERIALIZABLE 是"基于 MVCC 快照 + 谓词锁检测"的进阶 OCC。

PostgreSQL SERIALIZABLE 本质是 OCC 思想——不在执行时加锁、提交时检测冲突。但 SSI 用谓词锁扩展了检测范围,能发现写偏斜等更微妙的串行化异常。

#
★★

12. 乐观并发控制(OCC)与 MVCC 快照隔离的关系,为什么写冲突在提交时检测而读不阻塞?OCC 与 SSI 的冲突检测有何异同?

乐观并发控制(OCC)与 MVCC 快照隔离的关系是什么?为什么写冲突在提交时检测而读不阻塞?OCC 与 SSI 的冲突检测有何异同?

  • OCC 与 MVCC 快照隔离的关系
  • 写冲突提交时检测
  • OCC 与 SSI 差异

OCC 与 MVCC 快照隔离天然契合:MVCC 的快照读让事务在各自版本上执行、互不阻塞,这为 OCC 提供了"假设无冲突"的执行基础;OCC 在提交时检测写冲突,若发现其他事务已修改了同一数据则失败重试。之所以写冲突在提交时检测而读不阻塞,是因为 MVCC 让读操作看到一致快照、无需加锁,冲突只可能发生在写之间,推迟到提交时统一验证,可最大化并发。OCC 与 SSI 的异同:两者都"提交时检测",但 SSI 用谓词锁检测更广义的串行化异常(写偏斜),而普通 OCC 用版本号只检测写写冲突,SSI 保证更强。

MVCC 快照是 OCC 的"最佳拍档",读不阻塞、写冲突提交时验证。SSI 是 OCC 的增强版,用谓词锁扩大检测范围,保证可串行化。

#
★★

13. 乐观并发控制(OCC, Optimistic Concurrency Control)的原理,假设无冲突,提交时验证?

乐观并发控制(OCC)的原理是什么?为什么假设无冲突并在提交时验证?

  • OCC 原理
  • 假设无冲突
  • 提交时验证

乐观并发控制(OCC)的原理是:假设事务之间很少发生冲突,因此事务在读写阶段不加锁、自由执行,只在提交时验证是否存在冲突(即验证读过的数据是否在事务执行期间被其他事务修改)。若验证通过则提交,若冲突则回滚重试。OCC 的"假设无冲突、提交时验证"适合读多写少、冲突率低的场景,因为绝大多数事务可以无阻塞完成,获得高并发。代价是冲突率高时频繁重试,浪费资源。OCC 与悲观锁(提前加锁)相比,是"用重试换并发"。

OCC 的核心是"推迟冲突检测到提交时",通过重试换取无锁并发。它适用于低冲突场景,冲突率高时应改用悲观锁。

#
★★

14. OCC 与悲观锁(Pessimistic Locking)的取舍?

OCC 与悲观锁(Pessimistic Locking)的取舍是什么?如何选择?

  • OCC 与悲观锁的对比
  • 冲突率、并发度、重试
  • 适用场景

OCC 与悲观锁的取舍核心在于"冲突率与并发度"。OCC 在低冲突率下获得高并发(无锁执行、提交验证),但在高冲突率下频繁重试、浪费资源;悲观锁在高冲突率下能保证事务有序执行、减少重试,但会加锁阻塞、降低并发。选择依据:若业务读多写少、冲突率低(如个人资料更新),用 OCC;若写冲突频繁、数据一致性要求高(如扣库存、转账),用悲观锁(FOR UPDATE)或行锁。此外悲观锁会产生锁等待与死锁风险,OCC 无锁但需重试。应结合业务并发特征权衡。

"冲突率"是选择 OCC 还是悲观锁的关键变量。低冲突用 OCC 换并发,高冲突用悲观锁换稳定,需结合具体业务权衡。

#
★★

15. 版本号(Version Number)与时间戳的实现?

版本号(Version Number)与时间戳是如何实现乐观锁的?

  • 版本号实现
  • 时间戳实现
  • 更新时验证

版本号实现:表中加一个 version 列,初始为 0;读取时获取 version,更新时执行 UPDATE ... SET ..., version = version + 1 WHERE id = ? AND version = 旧版本;若影响行数为 0,说明 version 已被其他事务修改,冲突,需重试。时间戳实现类似:加一个 updated_at 列,更新时用 WHERE updated_at = 读取时的旧时间戳,若时间戳不匹配则冲突。两者都是"读取时记录基线、更新时验证基线是否变化"的乐观锁实现。版本号需配合事务(先读后写在同一事务)保证原子性。

版本号与时间戳都是乐观锁的"基线验证"实现。版本号递增更精确,时间戳需保证精度。更新时 WHERE 条件匹配失败即冲突,是 OCC 在 SQL 层面的落地。

#
★★

16. 分布式数据库(如 TiDB)的乐观事务如何在提交阶段检测写写冲突?与悲观锁模式相比在冲突率与重试上的取舍是什么?

分布式数据库(如 TiDB)的乐观事务如何在提交阶段检测写写冲突?与悲观锁模式相比有何取舍?

  • TiDB 乐观事务提交检测
  • 写写冲突检测
  • 与悲观锁的取舍

TiDB 的乐观事务在提交阶段通过"冲突检测"来验证写写冲突:事务提交时,TiDB 会检查本事务写入的 key 是否在事务执行期间被其他事务修改(通过主键冲突、时间戳比较等检测),若冲突则返回冲突错误,事务回滚重试。这种"提交时检测"就是 OCC 在分布式数据库的实现。与悲观锁模式(TiDB 也支持 SELECT ... FOR UPDATE 悲观锁)相比:乐观模式在低冲突率下并发高、无加锁阻塞,但冲突率高时重试频繁、浪费资源;悲观模式在执行时加锁、冲突少、重试少,但会阻塞、有锁等待。生产环境 write-heavy 场景常需选择悲观锁或结合两者。

分布式数据库的乐观事务同样遵循"提交时验证"原则,但需在分布式环境下检测冲突。乐观与悲观的选择取决于冲突率与重试成本,写冲突高的应用应倾向悲观锁。

#
★★

17. PostgreSQL 将 SIREAD 谓词锁合并到页级或表级后,为什么可能误杀本可串行化执行的事务?误报率与锁开销如何权衡?

PostgreSQL 将 SIREAD 谓词锁合并到页级或表级后,为什么可能误杀本可串行化执行的事务?误报率与锁开销如何权衡?

  • SIREAD 谓词锁合并
  • 合并导致误报
  • 误报率与锁开销权衡

当 PostgreSQL 将 SIREAD 谓词锁从元组级合并到页级或表级时,锁的范围变大了:原本只锁某几个元组,合并后锁整个页或整个表。这样本可串行化执行的两个事务,因为谓词锁粒度变粗而互相"冲突",SSI 可能误报串行化异常,终止本可安全执行的事务。这就是误杀。权衡在于:粒度越细(元组级)误报越少但锁开销越大;粒度越粗(页级/表级)锁开销小但误报率上升。PostgreSQL 通过自适应合并策略,在锁数量/内存压力大时提高粒度,接受一定误报率来换取性能。

谓词锁合并是"用误报率换锁开销"的折中。理解这一权衡,能解释为什么 SSI 在压力大时可能无端终止事务,以及如何通过参数(如 max_predicate_locks_per_relation)调整。

#

18. OCC 的 ABA 问题,版本号方案为何能避免 CAS 式比较中的 ABA?

OCC 的 ABA 问题是什么?为什么版本号方案能避免 CAS 式比较中的 ABA?

  • ABA 问题
  • 版本号避免 ABA
  • 与 CAS 对比

ABA 问题是 CAS(比较并交换)类并发控制中的经典问题:值从 A 变为 B 再变回 A,比较时看到仍是 A,误以为没被修改,从而错误地覆盖了中间的变化。OCC 的版本号方案能避免 ABA,因为版本号是单调递增的:即使数据值回到 A,版本号也已被递增(A→B→A 对应版本 1→2→3),读取时记录的版本号与更新时验证的版本号不匹配,冲突被检测到。因此版本号能区分"值恢复"与"从未修改",克服了单纯比较值无法识别的 ABA 问题。

ABA 问题本质是"值比较无法感知中间变化"。版本号作为"修改代数"的单调计数器,能识别值虽恢复但已被修改的情况,是 OCC 中避免 ABA 的关键。

#

19. 乐观并发控制的提交期验证有 forward 与 backward 两种方式,它们验证的对象与适用读写比有何差异?

乐观并发控制的提交期验证有 forward 与 backward 两种方式,它们验证的对象与适用读写比有何差异?

  • backward 验证
  • forward 验证
  • 适用读写比

乐观并发控制的提交期验证分为 backward(向后验证)与 forward(向前验证)两种。Backward 验证:事务提交时,验证自己读过的数据是否被"已提交事务"(在自己执行期间提交的其他事务)修改,即往回检查已提交事务,适用于读多写少的场景(读事务多,验证成本低)。Forward 验证:事务提交时,验证自己写的数据是否会被"尚未提交的事务"(当前活跃事务)读取/修改,即往前检查活跃事务,适用于写多读少的场景。Backward 侧重"读过的数据是否被改",Forward 侧重"写的数据是否影响他人",两者验证对象不同,适用读写比也相反。

backward 与 forward 是 OCC 提交验证的两种方向。Backward 适合读多写少(读事务多、验证效率高),Forward 适合写多读少,选择取决于事务的读写特性。