面试考察点
- 能否区分共享锁、排他锁、记录锁、间隙锁和 Next-Key Lock。
- 是否理解 InnoDB 所谓“行锁”实际加在索引记录上。
- 能否结合隔离级别、索引与查询条件分析锁范围。
- 是否知道死锁不是简单的“两个事务同时更新”。
- 是否具备从等待关系、SQL、事务和索引定位线上问题的能力。
锁的维度
从兼容性看,共享锁允许多个事务读取同一资源,排他锁与其他共享或排他锁冲突。普通快照读通常不加记录共享锁,SELECT ... FOR SHARE、FOR 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 后失败。两者排查方向相关但不完全相同。
线上排查步骤
- 保存应用错误中的事务、SQL、参数和时间点。
- 查看
SHOW ENGINE INNODB STATUS的最近死锁信息。 - 使用 Performance Schema 的事务、数据锁和锁等待视图还原持有者与等待者。
- 查看事务开始时间和当前语句,警惕已经空闲但未提交的会话。
- 对照执行计划确认扫描与加锁范围。
- 检查代码中多表、多行访问顺序是否一致。
不要只盯着最后报错的 SQL。它可能只是等待方,真正问题是另一个事务更早执行的语句或未及时提交。
降低死锁和阻塞的方法
- 按固定顺序访问表与记录。
- 通过高选择性索引缩小扫描和加锁范围。
- 减小事务,每次只包含必须原子提交的数据库操作。
- 不在事务中做 HTTP 调用、消息等待或人工交互。
- 大批量更新按主键分批,避免一次锁住过多记录。
- 使用合理超时,让失败快速暴露;重试必须有限且幂等。
核心考点清单
- InnoDB 行锁依赖索引,执行计划决定实际扫描与锁定范围。
- Next-Key Lock 是记录锁与间隙锁的组合,常用于范围当前读。
- RR 和 RC 在一致性语义及间隙锁使用上存在差异。
- 死锁的根因是资源等待形成环,统一加锁顺序能破坏循环等待。
- 排查必须还原完整事务,不应只分析最后一条 SQL。
高频追问与参考回答
追问 1:普通 SELECT 会加行锁吗?
InnoDB 的普通一致性读通常通过 MVCC 读取快照,不加记录锁。显式 FOR UPDATE、FOR 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」、配置实验和事故数据,比复述固定模板更有说服力。