面试考察点

  • 能否区分共享锁、排他锁、记录锁、间隙锁和 Next-Key Lock。
  • 是否理解 InnoDB 所谓“行锁”实际加在索引记录上。
  • 能否结合隔离级别、索引与查询条件分析锁范围。
  • 是否知道死锁不是简单的“两个事务同时更新”。
  • 是否具备从等待关系、SQL、事务和索引定位线上问题的能力。

锁的维度

从兼容性看,共享锁允许多个事务读取同一资源,排他锁与其他共享或排他锁冲突。普通快照读通常不加记录共享锁,SELECT ... FOR SHAREFOR UPDATE 属于锁定读。

从作用范围看,InnoDB 常见锁包括:

  • Record Lock:锁住某条索引记录。
  • Gap Lock:锁住两个索引值之间的间隙,阻止插入,不锁定已有记录本身。
  • Next-Key Lock:记录锁与其前方间隙的组合。
  • Insert Intention Lock:插入前对目标间隙表达插入意图,不同位置的插入通常可并发。
  • Intention Lock:表级意向锁,表示事务准备在表中某些行加共享或排他锁,帮助快速判断表锁兼容性。

行锁为什么与索引有关

InnoDB 通过索引定位并锁定记录。若更新条件缺少合适索引,执行器可能扫描大量索引记录,并在过程中锁住远超预期的数据。即使最终只更新一行,也可能造成大范围阻塞。

UPDATE orders SET status = 'CLOSED'
WHERE external_no = 'X20260721001';

external_no 没有索引,数据库需要扫描更多记录。线上写操作条件必须确认索引与选择性,而不是只看受影响行数。

唯一索引等值查询的锁范围

当使用唯一索引精确命中已存在记录时,通常可以退化为记录锁,不必锁住整个间隙。若查询的唯一键不存在,为防止其他事务插入满足条件的记录,仍可能锁住目标间隙。

锁范围受 MySQL 版本、隔离级别、索引是否唯一、条件是否等值、记录是否存在和实际执行计划影响。面试回答应给出分析维度,不要死背一个范围覆盖所有场景。

RC 与 RR 的差异

Repeatable Read 为了支持当前读的范围一致性,会更广泛使用 Gap Lock 和 Next-Key Lock。Read Committed 通常减少间隙锁使用,从而提高并发,但同一事务两次读取可能看到不同已提交版本。

切换隔离级别不是单纯性能开关。必须确认业务是否依赖重复读语义、唯一性检查以及现有并发控制逻辑。

死锁的四个条件

死锁通常满足互斥、占有且等待、不可剥夺、循环等待。数据库中最常见的形态是两个事务以相反顺序持有并请求资源:

事务 A:锁订单 1 → 等待订单 2
事务 B:锁订单 2 → 等待订单 1

InnoDB 会检测等待图中的环,并选择一个事务回滚以打破死锁。被回滚不代表数据库故障,应用应对死锁错误做有限、带退避且幂等的重试。

一个典型死锁案例

-- 事务 A
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

-- 事务 B
UPDATE account SET balance = balance - 50 WHERE id = 2;
UPDATE account SET balance = balance + 50 WHERE id = 1;

两个转账事务按不同账户顺序加锁。解决方法是统一按较小账户 ID 到较大账户 ID 加锁。统一顺序消除循环等待,同时事务仍要保持短小。

死锁与锁等待超时的区别

死锁存在等待环,数据库通常能快速检测并回滚一个事务。锁等待超时可能只是某事务长时间持锁,并不存在环;等待方达到 innodb_lock_wait_timeout 后失败。两者排查方向相关但不完全相同。

线上排查步骤

  1. 保存应用错误中的事务、SQL、参数和时间点。
  2. 查看 SHOW ENGINE INNODB STATUS 的最近死锁信息。
  3. 使用 Performance Schema 的事务、数据锁和锁等待视图还原持有者与等待者。
  4. 查看事务开始时间和当前语句,警惕已经空闲但未提交的会话。
  5. 对照执行计划确认扫描与加锁范围。
  6. 检查代码中多表、多行访问顺序是否一致。

不要只盯着最后报错的 SQL。它可能只是等待方,真正问题是另一个事务更早执行的语句或未及时提交。

降低死锁和阻塞的方法

  • 按固定顺序访问表与记录。
  • 通过高选择性索引缩小扫描和加锁范围。
  • 减小事务,每次只包含必须原子提交的数据库操作。
  • 不在事务中做 HTTP 调用、消息等待或人工交互。
  • 大批量更新按主键分批,避免一次锁住过多记录。
  • 使用合理超时,让失败快速暴露;重试必须有限且幂等。

核心考点清单

  • InnoDB 行锁依赖索引,执行计划决定实际扫描与锁定范围。
  • Next-Key Lock 是记录锁与间隙锁的组合,常用于范围当前读。
  • RR 和 RC 在一致性语义及间隙锁使用上存在差异。
  • 死锁的根因是资源等待形成环,统一加锁顺序能破坏循环等待。
  • 排查必须还原完整事务,不应只分析最后一条 SQL。

高频追问与参考回答

追问 1:普通 SELECT 会加行锁吗?

InnoDB 的普通一致性读通常通过 MVCC 读取快照,不加记录锁。显式 FOR UPDATEFOR SHARE 或写操作属于当前读,需要相应锁。

追问 2:没有索引的 UPDATE 是不是锁表?

技术上通常仍是对扫描到的索引记录加锁,不等同于显式表锁;但因为可能扫描并锁住大量记录,效果上会严重阻塞并发,常被口语化为“像锁表”。

追问 3:发生死锁应该把超时时间调大吗?

调大锁等待超时不能消除等待环。应统一资源顺序、缩小锁范围和事务,并对被选为牺牲者的事务做安全重试。

追问 4:间隙锁锁住的是不存在的记录吗?

更准确地说,它锁定索引值之间的范围,阻止其他事务向该范围插入,而不是锁住一个虚构行。不同事务的间隙锁在某些情况下可以兼容。

追问 5:如何快速发现长事务?

查看 InnoDB 事务视图、会话状态与事务开始时间,并结合 Undo 历史长度、锁等待和应用链路。连接处于 Sleep 不代表没有打开事务。

机制全景图

下面把「MySQL 锁机制、死锁与线上排查详解」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。

flowchart LR
    A["事务定位索引记录"]
    A --> B["申请记录或间隙锁"]
    B --> C["冲突后进入等待"]
    C --> D["等待图形成环或超时"]
    D --> E["选择牺牲者回滚"]

完整链路:从输入到结果

沿着「事务定位索引记录 → 申请记录或间隙锁 → 冲突后进入等待 → 等待图形成环或超时 → 选择牺牲者回滚」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。

1. 事务定位索引记录

InnoDB 锁作用于索引记录;没有合适索引时扫描和锁定范围可能远超业务认知。

2. 申请记录或间隙锁

Record、Gap 与 Next-Key Lock 组合用于记录互斥和防止特定范围幻读,隔离级别与语句类型会改变行为。

3. 冲突后进入等待

锁冲突线程等待持有者释放,长事务和外部 I/O 会放大等待链。

4. 等待图形成环或超时

两个事务以不同顺序获取资源会形成 wait-for 环,InnoDB 检测后选择成本较小事务回滚。

5. 选择牺牲者回滚

死锁是并发控制的正常可恢复结果,应用应识别错误码、回滚完整事务并做有界重试。

源码与实现定位

入口 阅读重点
performance_schema.data_locks 当前锁对象与模式
SHOW ENGINE INNODB STATUS LATEST DETECTED DEADLOCK 图

源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。

参数配置与可复现实验

SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;

两个事务反序更新相同 ID,复现死锁;加排序与索引后循环并发 10 万次比较死锁率。

验证步骤与预期结果

1. 固定输入和基线

先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「事务 P99」为主基线,记录值应满足「低于接口预算」;同时保存 锁等待时间、死锁日志频率,使后续变化能够回到同一时间轴比较。

2. 从实现入口确认路径

在「performance_schema.data_locks」确认请求确实进入「当前锁对象与模式」对应的实现,再沿「SHOW ENGINE INNODB STATUS」观察「LATEST DETECTED DEADLOCK 图」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。

3. 注入本文特有的失败模式

优先复现「缺少索引导致锁范围扩大」,并把单一变量逐级放大,直到「事务 P99」越过「秒级」。随后再分别验证「事务内调用远程服务长期持锁」和「捕获死锁后只重试单条语句」,三类故障分开执行,避免多个变量同时变化而无法归因。

4. 执行止损和根因修复

第一轮只应用「按主键排序更新」,确认它能控制影响范围;第二轮应用「远程调用移出事务」,验证核心链路恢复;最后落实「完整事务有界退避重试」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。

5. 通过退出条件

实验只有同时满足三项才算通过:「事务 P99」回到「低于接口预算」、「死锁率」回到「低且可重试」、「锁定/返回行」回到「接近业务行数」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。

量化基线

指标 样例基线/口径 风险线 结论
事务 P99 低于接口预算 秒级 持锁过长
死锁率 低且可重试 发布后翻倍 顺序变化
锁定/返回行 接近业务行数 数量级放大 缺索引

这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。

事故复盘:批量更新顺序不一致导致持续死锁

两个订单任务分别按输入顺序更新商品库存,相同商品集合顺序不同,形成交叉锁。提交前按 sku_id 排序并缩短事务后,死锁显著下降;重试仍保留以处理其他不可避免冲突。

失败模式 首要证据 第一处置动作
缺少索引导致锁范围扩大 锁等待时间 按主键排序更新
事务内调用远程服务长期持锁 死锁日志频率 远程调用移出事务
捕获死锁后只重试单条语句 事务持续时间 完整事务有界退避重试

发布与回滚检查点

  • 发布前:确认「performance_schema.data_locks」对应实现和上述配置在目标版本仍然有效,并保存「事务 P99」基线。
  • 灰度中:同时观察 锁等待时间、死锁日志频率、事务持续时间;任一指标越过表中风险线,就停止继续扩量。
  • 回滚时:先执行「按主键排序更新」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
  • 发布后:至少覆盖一个完整峰值周期,确认「缺少索引导致锁范围扩大」没有再次出现,才关闭变更观察窗口。

方案对比与选型

方案 更适合的场景 主要收益 代价与边界
一致加锁顺序 同类批量资源 从设计上减少环 所有路径必须遵守
短事务 + 有界重试 冲突偶发且操作幂等 恢复简单、吞吐较好 重试会增加瞬时负载
串行化/分区队列 热点键冲突极高 消除同键并发竞争 增加排队与分区治理

选型至少带上 数据规模、选择性、读写比、事务长度和峰值并发,并用上面的量化基线验证;未知数据应明确为待测假设。

设计边界与工程取舍

死锁回滚的是完整事务,不是“失败的那条 SQL”;重试必须从事务边界重新开始并确保外部副作用幂等。

工程落地遵循:正确性由约束和事务兜底,性能优化必须用执行计划与测量验证。回答时直接引用「performance_schema.data_locks」、配置实验和事故数据,比复述固定模板更有说服力。