1.5.4 InnoDB(事务存储引擎)锁与死锁:从索引区间到等待证据
读完本分册,你能对一条并发 SQL(结构化查询语言)说清:它走哪棵索引、锁住哪些索引记录和间隙、谁在等谁、为什么会超时或死锁,以及库存扣减、支付幂等、订单状态推进怎样在数据库边界内正确落地。
0. 使用边界、版本口径与复习目标
本分册适用于 MySQL(关系型数据库)5.7、8.0 与 8.4 的 InnoDB(事务存储引擎)。核心结论是:锁范围由“隔离级别、是否当前读、访问索引、唯一性、谓词和实际扫描终点”共同决定;不能只按 where(条件子句)表面文字推断。Read Committed(读已提交,以下简称 RC(读已提交))通常减少间隙保护,Repeatable Read(可重复读,以下简称 RR(可重复读))在锁定读与写入路径使用记录、间隙或临键保护来处理幻读边界。小版本的诊断视图字段有差异,线上以实机 performance_schema(性能模式) 为准。
| 复习目标 | 你必须能复述 | 常见误区 |
|---|---|---|
| 锁对象 | InnoDB(事务存储引擎)锁索引记录与索引间隙 | 说成“锁住一行 Java(编程语言)对象” |
| 锁范围 | 访问路径和扫描终点决定范围 | 只看业务条件、不看索引 |
| 并发故障 | 等待是有向边,死锁是等待环 | 把所有等待都叫死锁 |
| 工程落地 | 条件更新、唯一约束、顺序和重试协作 | 只靠应用层互斥 |
flowchart LR
A["SQL(结构化查询语言)与隔离级别"] --> B["执行计划与访问索引"]
B --> C["当前读扫描索引记录/间隙"]
C --> D["申请 S(共享)或 X(排他)锁"]
D --> E["成功、锁等待、超时或死锁"]
E --> F["业务条件/幂等/补偿裁决"]1. 锁的对象、层次与兼容性
InnoDB(事务存储引擎)的行级并发控制以索引为载体:聚簇索引记录、二级索引记录及其键值之间的间隙才是主要对象。S(共享锁)允许多个事务读同一对象但阻塞冲突写;X(排他锁)阻塞其他 S(共享锁)与 X(排他锁)。表级 IS(意向共享锁)和 IX(意向排他锁)不是“先锁表再锁行”,而是声明事务将对表内某些索引记录持有 S(共享锁)或 X(排他锁),使表锁或 DDL(数据定义语言)快速判断是否存在行锁意图。它们与行锁的粒度不同,不能把兼容矩阵机械地解释成业务权限。
| 请求\已持有 | S(共享锁) | X(排他锁) | IS(意向共享锁) | IX(意向排他锁) |
|---|---|---|---|---|
| S(共享锁) | 兼容 | 冲突 | 兼容 | 冲突 |
| X(排他锁) | 冲突 | 冲突 | 冲突 | 冲突 |
| IS(意向共享锁) | 兼容 | 冲突 | 兼容 | 兼容 |
| IX(意向排他锁) | 冲突 | 冲突 | 兼容 | 兼容 |
flowchart TD
T["事务 T(事务)"] --> IX["IX(意向排他锁):表层声明"]
IX --> R["二级索引/聚簇索引记录"]
R --> X["X(排他锁):实际修改保护"]
DDL["DDL(数据定义语言)申请 MDL(元数据锁)"] -.检查兼容性.-> IX热门面试题
问题(基础题):S(共享锁)、X(排他锁)、IS(意向共享锁)和 IX(意向排他锁)分别解决什么问题?
- 考点:锁模式、粒度和兼容性。
- 回答思路:先分记录级读写冲突,再说明表级意向声明。
- 详细答案:S(共享锁)保护可锁定读取,X(排他锁)保护修改;IS(意向共享锁)与 IX(意向排他锁)告诉引擎表内已有或即将有记录锁。意向锁不替代记录锁,也不是业务上的表锁。
- 进阶追问:为什么 IX(意向排他锁)能与 IX(意向排他锁)兼容?
- 进阶回答:两个事务都“打算修改某些记录”并不代表修改同一索引记录;真正的冲突在后续记录或范围锁上裁决。
问题(原理题):为什么说 InnoDB(事务存储引擎)锁的是索引,不是逻辑行?
- 考点:访问路径与物理锁对象。
- 回答思路:说明 B+Tree(多路平衡树)定位与二级索引回表。
- 详细答案:存储引擎沿索引键扫描并在索引记录、间隙上登记锁;使用二级索引更新时常会同时涉及二级索引和聚簇索引。没有可用索引时扫描大量记录,锁覆盖面看似“全表”,但不是行锁自动升级。
- 进阶追问:是否存在表锁?
- 进阶回答:存在显式表锁、MDL(元数据锁)和其他引擎机制;这与 InnoDB(事务存储引擎)把大量记录锁自动升级为表锁是两件事。
问题(项目题):库存扣减为什么不应先
select(查询)再在应用内判断?- 考点:读写原子性与条件更新。
- 回答思路:把判断放进 X(排他锁)保护的同一条更新。
- 详细答案:WMS(仓储管理系统)应执行带库存条件的原子更新,并以影响行数判断是否预占成功。先普通查询会读到未被本事务锁住的值,两个请求可同时判断“够用”;条件更新让后到事务在当前记录上等待,醒来后重新检查条件。
- 进阶追问:条件更新成功是否等于请求绝不重复?
- 进阶回答:不等于;还要以业务请求号或预占单号的唯一约束保证重复提交不重复扣减。
2. 记录锁、间隙锁、临键锁与插入意向锁
记录锁只锁一个已经存在的索引记录,例如 [20];间隙锁只锁两个索引记录之间的开区间,例如 (10,20),它保护“不能在这里插入”,不是锁住已有行;临键锁是前一个间隙加当前记录,例如 (10,20]。插入意向锁是插入前在目标间隙声明“我要放入某个位置”的特殊间隙意图:两个事务向同一间隙插入不同键通常可并行,但若已有间隙锁或临键锁覆盖该位置,就必须等待。它不是把一个间隙锁住的排他锁,也不等于插入已成功。

| 锁类型 | 精确对象 | 主要目的 | 典型边界 |
|---|---|---|---|
| 记录锁 | 已存在索引记录 | 防止该记录被冲突读写 | [20] |
| 间隙锁 | 两条记录间空档 | 抑制该空档插入 | (10,20) |
| 临键锁 | 前间隙加当前记录 | 范围当前读的幻读保护 | (10,20] |
| 插入意向锁 | 待插入所在间隙 | 排队表达插入位置 | INSERT(插入) 15 位于 (10,20) |
sequenceDiagram
participant T1 as T1(事务)
participant I as 二级索引键 10,20,30
participant T2 as T2(事务)
T1->>I: RR(可重复读)范围当前读,锁 (10,20]
T2->>I: INSERT(插入) 15,申请插入意向锁
I-->>T2: 等待:目标间隙被 T1 覆盖
T1->>I: COMMIT(提交)
I-->>T2: 获得位置并继续插入热门面试题
问题(基础题):记录锁、间隙锁、临键锁怎样用区间区分?
- 考点:索引键边界。
- 回答思路:以键
10、20、30画出闭开区间。 - 详细答案:记录锁是
[20],间隙锁是(10,20),临键锁是两者组合(10,20]。区间端点对应索引顺序,不是业务时间范围;最小与最大边界还涉及 Infimum(下确界伪记录)和 Supremum(上确界伪记录)。 - 进阶追问:间隙锁会阻塞同一间隙的另一个间隙锁吗?
- 进阶回答:不同事务的间隙锁通常可以共存,因为它们共同目标是阻止插入;真正需要重点观察的是插入意向锁与已有间隙保护的冲突。
问题(原理题):插入意向锁为什么存在?
- 考点:插入定位与并发排队。
- 回答思路:说明插入前先确定间隙、再写入记录。
- 详细答案:它让引擎区分“多个事务准备在同一大间隙不同位置插入”和“有人已经用范围锁禁止该间隙插入”。前者无需互相串行,后者必须等保护者结束,从而兼顾插入并发和范围语义。
- 进阶追问:插入意向锁能绕过临键锁吗?
- 进阶回答:不能;它正是被覆盖该位置的间隙锁或临键锁阻塞的等待请求。
问题(项目题):订单状态时间线索引为什么容易出现间隙等待?
- 考点:普通二级索引与范围扫描。
- 回答思路:说明按状态、时间范围锁定任务与并发插入新订单。
- 详细答案:Runner(执行器)调度若按普通索引的
status + next_run_time(状态加下次执行时间)做范围锁定扫描,同时新任务落在扫描间隙,RR(可重复读)下可能等待。应缩小批次、使用稳定排序键、尽快提交,并评估 RC(读已提交)语义是否满足任务领取模型。 - 进阶追问:能否用
skip locked(跳过已锁定记录)? - 进阶回答:可以用于可并行领取且允许跳过的任务模型;仍要保留状态条件、租约或超时回收,不能把跳过当成任务已经完成。
3. 主键、唯一索引、普通索引与无索引访问路径
同一逻辑条件因为索引不同而锁区间不同。唯一索引的等值当前读命中已存在唯一键时,通常可精确为记录锁;唯一键查找不存在时,RR(可重复读)为防止随后出现该键,需要保护其所在间隙。普通二级索引的键允许重复,等值查找必须扫描同值记录及其边界,常出现临键范围;更新二级索引定位到的行还会锁聚簇索引记录。无合适索引时,引擎为找出符合条件的记录而扫描大量聚簇索引条目,锁扩大源于扫描,并非阈值触发的自动“表锁升级”。
| 访问条件 | 索引形态 | 当前读的主要锁对象 | 解释 |
|---|---|---|---|
id=20 | 主键 | 聚簇索引记录 [20] | 唯一且存在时可精确定位 |
request_no='P1' | 唯一二级索引 | 唯一二级记录及对应聚簇记录 | 防重与回表修改都要保护 |
status='NEW' | 普通二级索引 | 同值记录和扫描边界的索引区间 | 重复键使边界更宽 |
remark='x' | 无索引 | 扫描到的聚簇索引记录/范围 | 代价与阻塞面都可能扩大 |
flowchart TD
Q["WHERE(条件子句) id=20"] --> P["主键唯一定位"] --> L1["记录锁为主"]
Q2["WHERE(条件子句) status='NEW'"] --> S["普通二级索引扫描"] --> L2["同值区间与边界锁"]
Q3["无索引谓词"] --> C["聚簇索引全扫描"] --> L3["大量扫描记录受影响"]热门面试题
问题(基础题):主键等值更新与普通索引等值更新的锁范围为何不同?
- 考点:唯一性与重复键扫描。
- 回答思路:先说是否能唯一定位,再说扫描边界。
- 详细答案:主键唯一命中可直接定位一个聚簇索引记录;普通索引的同一个键可对应多条记录,引擎要扫描同值组并维护范围边界。实际范围还取决于 RC(读已提交)或 RR(可重复读)、语句类型和执行计划。
- 进阶追问:普通二级索引修改为什么还会碰聚簇索引?
- 进阶回答:二级索引叶子保存主键,最终更新行内容或校验版本通常要定位聚簇记录;修改二级键本身还要维护旧、新二级条目。
问题(原理题):唯一索引等值查找一个不存在的值,为什么仍可能等待?
- 考点:不存在记录与间隙保护。
- 回答思路:找出目标键所在的前后索引记录。
- 详细答案:不存在的记录没有记录锁可加,但其“将来可能插入的位置”位于一个索引间隙。RR(可重复读)锁定读或写入为保持谓词稳定,可能保护该间隙,因此另一个事务插入相同或落在该间隙的键会等待。
- 进阶追问:这是否意味着任何普通
select(查询)都锁间隙? - 进阶回答:不是;普通一致性读主要由 MVCC(多版本并发控制)服务。这里讨论的是当前读、锁定读、更新或删除的锁路径。
问题(项目题):支付回调以渠道流水号做唯一索引,怎样利用它保证幂等?
- 考点:唯一约束与状态转换。
- 回答思路:先用唯一键建立事实,再用条件状态推进。
- 详细答案:支付流水表对
channel_trade_no(渠道交易流水号)设唯一索引,首个请求写入事实;重复请求命中唯一冲突后查询既有处理结果。订单推进仍使用带前置状态和金额校验的条件更新,不能仅因“唯一插入失败”就假设每一步外部副作用都已完成。 - 进阶追问:重复回调遇到死锁能直接无限重试吗?
- 进阶回答:不能;仅对幂等、可重入步骤做有限退避重试,超过阈值进入可观测的补偿队列并以流水对账收口。
4. 当前读、快照读与幻读防护边界
普通 select(查询)通常是由 MVCC(多版本并发控制)提供的一致性读或快照读;select ... for update(查询后更新锁定)、select ... for share(查询后共享锁定)、update(更新)、delete(删除)和 insert(插入)则面向最新可用版本执行当前读,并申请必要锁。RR(可重复读)中,一致性读依靠 Read View(读视图)使同一快照结果稳定;当前读通过记录、间隙、临键锁防止目标谓词范围出现可见的新行。RC(读已提交)中每次一致性读可见最新已提交版本,锁定读通常不保留大多数间隙锁,但外键检查、重复键检查等仍有例外,不能简化成“RC(读已提交)绝无间隙锁”。
| 读写动作 | 是否当前读 | RR(可重复读)重点 | RC(读已提交)重点 |
|---|---|---|---|
普通 select(查询) | 否 | 同一事务复用读视图 | 每次一致性读可更新读视图 |
for update(更新锁定) | 是 | 记录与范围保护幻读边界 | 记录锁为主,间隙例外需核实 |
update(更新)/delete(删除) | 是 | 按访问路径锁扫描对象 | 不匹配记录可较早释放 |
insert(插入) | 是 | 插入意向与唯一检查 | 同样受唯一/外键检查约束 |
flowchart LR
R["普通查询"] --> V["MVCC(多版本并发控制)快照读"] --> O["历史或可见版本"]
W["更新/锁定查询"] --> C["当前读"] --> L["索引记录与间隙锁"] --> N["最新记录和条件裁决"]热门面试题
问题(基础题):当前读和快照读的根本差别是什么?
- 考点:版本可见性与写入裁决。
- 回答思路:以读取目标和是否申请锁分别回答。
- 详细答案:快照读从 Read View(读视图)构造对事务可见的版本,主要服务非锁定查询;当前读读取最新记录以便正确写入或锁定,并通过锁解决并发冲突。库存扣减必须以当前读语义的条件更新裁决,不能用旧快照判断成功。
- 进阶追问:RR(可重复读)是否让所有查询永远看不到幻读?
- 进阶回答:同一快照的一致性读结果稳定;当前读要靠实际锁范围处理,SQL(结构化查询语言)与索引变化仍会改变业务可观察结果,不能脱离读类型下绝对结论。
问题(原理题):RR(可重复读)的临键锁怎样防止幻读?
- 考点:谓词范围与插入抑制。
- 回答思路:说明扫描到的索引记录加前间隙。
- 详细答案:范围当前读把访问到的索引记录和相邻前间隙作为保护对象,其他事务不能在被覆盖间隙插入满足相同范围的新索引键。锁住的是索引区间而非
where(条件子句)的抽象文本,扫描终点之外并不自动受保护。 - 进阶追问:锁范围完全由优化器预估行数决定吗?
- 进阶回答:不完全;预估会影响访问路径选择,真正锁范围仍以执行时实际扫描的索引记录、边界和隔离语义为准。
问题(项目题):订单领取为什么要区分“可跳过”和“绝不能跳过”?
- 考点:锁定读与任务语义。
- 回答思路:把业务队列可并行性写进 SQL(结构化查询语言)与状态机。
- 详细答案:可并行的异步任务可用
skip locked(跳过已锁定记录)提高吞吐,但必须有租约、领取人和超时回收;支付入账这类必须严格顺序的状态推进不能静默跳过,而应等待、失败重试或按幂等键查询结果。 - 进阶追问:
skip locked(跳过已锁定记录)是否解决死锁? - 进阶回答:不能;它只避免等待已锁记录的一部分场景,多个资源的不同加锁顺序仍可形成等待环。
5. 自增锁、AUTO-INC(自增)模式与批量写入边界
AUTO_INCREMENT(自增列)分配值的并发语义由 innodb_autoinc_lock_mode(自增锁模式)影响。传统模式 0 对含自增列的插入持有表级 AUTO-INC(自增)锁直到语句结束;连续模式 1 对可预知行数的简单插入可短暂持锁分配连续编号,批量 insert ... select(插入查询结果)等不确定行数语句仍可能使用更强协调;交错模式 2 允许并发语句交错取得编号,吞吐更高但一个语句的编号不保证连续。自增锁解决的是编号分配,不是业务顺序、库存顺序或支付流水唯一性。
| 模式 | 名称 | 并发编号特征 | 适用提醒 |
|---|---|---|---|
0 | traditional(传统) | 语句间串行分配 | 并发低,兼容旧行为 |
1 | consecutive(连续) | 简单插入通常连续 | 常见默认口径,批量插入仍要评估 |
2 | interleaved(交错) | 并发语句可交错取号 | 吞吐优先,不能把编号连续当业务语义 |
sequenceDiagram
participant T1 as T1(事务)
participant A as AUTO-INC(自增)分配器
participant T2 as T2(事务)
T1->>A: 批量 INSERT(插入)申请编号
T2->>A: 并发 INSERT(插入)申请编号
Note over A: 模式决定等待时长与编号是否交错
A-->>T1: 分配一组编号
A-->>T2: 分配后续或交错编号热门面试题
问题(基础题):AUTO-INC(自增)锁和行锁有什么不同?
- 考点:编号分配与记录保护的职责边界。
- 回答思路:先说对象是自增计数分配,再说持锁时长由模式与语句决定。
- 详细答案:AUTO-INC(自增)锁协调自动编号分配,通常以语句为粒度;记录锁保护已经定位的索引记录或区间。自增值出现空洞是正常现象,回滚、并发或预分配都可能造成,不应以连续主键判断订单完整性。
- 进阶追问:高并发订单是否应依赖自增主键排序?
- 进阶回答:不应把它当业务时间线;需要业务创建时间、事件序号或独立全局标识,并明确时钟、分片和排序边界。
问题(原理题):为什么批量
insert ... select(插入查询结果)更需要关注自增锁?- 考点:待插入行数不确定。
- 回答思路:说明执行前不能准确预分配完整编号段。
- 详细答案:简单多值插入可预先知道行数,批量查询结果的行数在执行过程中才确定。为了保证复制与编号语义,引擎可能采用更保守的分配协调;线上要用实际模式、复制格式和压测结果验证,不要只套默认结论。
- 进阶追问:能否用更大批次减少锁竞争?
- 进阶回答:批次变大也会拉长语句、日志和回滚成本;应在吞吐、锁等待、复制延迟和失败重试成本间选取可回滚的批量。
问题(项目题):支付流水主键的设计为什么不能只依赖 AUTO_INCREMENT(自增列)?
- 考点:幂等身份与代理可观测性。
- 回答思路:区分内部物理主键和外部幂等键。
- 详细答案:自增主键适合聚簇索引局部写入,但渠道交易号、支付请求号才是防重复的业务身份,必须建唯一约束。支付回调重试时以外部唯一键查回原流水与状态;不能因为拿到新自增值就再次记账。
- 进阶追问:分库后自增主键如何处理?
- 进阶回答:分片环境应使用路由可识别的全局标识或号段方案,并将幂等键、分片键和对账键分开设计。
6. MDL(元数据锁)、表级协调与 DDL(数据定义语言)阻塞
MDL(元数据锁)由 MySQL(关系型数据库)服务器层管理,保护表定义在语句或事务期间不被并发改变。一个长事务即使只做普通 select(查询),也可能持有 MDL(元数据锁)共享读锁;随后排队的 alter table(修改表结构)需要更强元数据锁,再后来的新业务访问可能被排在 DDL(数据定义语言)之后,表现为“一个改表拖住整张表”。这与 InnoDB(事务存储引擎)的记录/间隙锁不同,排查要同时查看事务、元数据等待和正在执行或等待的 DDL(数据定义语言)。Online DDL(在线数据定义语言)只降低部分阶段的阻塞,不等于零 MDL(元数据锁)。
| 现象 | 直接原因 | 容易误判 | 首要证据 |
|---|---|---|---|
alter table(修改表结构)长期等待 | 旧事务持有兼容性不满足的 MDL(元数据锁) | 误以为是行锁 | 事务开始时间与元数据锁等待 |
| 新查询突然堆积 | 已排队的 DDL(数据定义语言)形成队列阻塞 | 只看 DDL(数据定义语言)慢 | 进程列表、MDL(元数据锁)队列 |
| 在线变更仍卡住 | 提交/切换阶段需更强锁 | 以为在线即无锁 | 变更阶段与业务长事务 |
sequenceDiagram
participant T1 as 长事务 T1(事务)
participant M as MDL(元数据锁)队列
participant D as DDL(数据定义语言)会话
participant Q as 新业务查询
T1->>M: 持有共享元数据锁
D->>M: ALTER(修改)请求排队等待排他元数据锁
Q->>M: 新访问排在 DDL(数据定义语言)之后
Note over T1,Q: 找到并结束异常长事务,不能先盲目杀 DDL(数据定义语言)热门面试题
问题(基础题):MDL(元数据锁)与 InnoDB(事务存储引擎)行锁的区别是什么?
- 考点:服务器层元数据保护与存储引擎索引锁。
- 回答思路:分别回答保护对象、触发语句和诊断视图。
- 详细答案:MDL(元数据锁)保护表、视图等定义不被并发 DDL(数据定义语言)破坏,通常由服务器层维护;行级锁保护索引记录和间隙,由 InnoDB(事务存储引擎)维护。两者可同时存在,不能只查一类视图。
- 进阶追问:普通查询为什么会阻塞改表?
- 进阶回答:事务内的访问需要保证提交前表定义稳定,长事务未结束就持续占用相应 MDL(元数据锁),使改表无法获得排他元数据锁。
问题(原理题):为什么一个等待中的 DDL(数据定义语言)会让后续查询也卡住?
- 考点:元数据锁队列公平性与排队效应。
- 回答思路:按“旧读锁—DDL(数据定义语言)排队—新读请求”解释。
- 详细答案:为避免 DDL(数据定义语言)永久饥饿,元数据锁队列不能无限让后来的共享请求插队。于是前面一个长事务和一个等待 DDL(数据定义语言)会放大为新业务请求堆积,需优先定位真正持锁的老事务。
- 进阶追问:线上应直接
kill(终止)谁? - 进阶回答:先保全会话、事务时长、SQL(结构化查询语言)和业务影响证据;优先与业务确认后结束异常长事务。终止 DDL(数据定义语言)只会解除队列,未必消除根因。
问题(项目题):订单表加索引如何避免 MDL(元数据锁)事故?
- 考点:变更窗口、长事务治理和回滚。
- 回答思路:先清长事务,再使用可观察、可中断的变更方案。
- 详细答案:变更前检查活跃事务、慢 SQL(结构化查询语言)和连接池事务边界;选择合适 Online DDL(在线数据定义语言)能力,在低峰执行并设等待阈值。大表可采用影子表或在线变更工具,但仍要验证最终切换 MDL(元数据锁)窗口并准备停止与回退。
- 进阶追问:加索引能否解决所有锁等待?
- 进阶回答:不能;索引可缩小 DML(数据操纵语言)扫描范围,却不能消除热点单行写冲突、长事务、MDL(元数据锁)或不合理的加锁顺序。
7. 谓词锁、空间索引与“边界不是万能临键锁”
谓词锁主要用于空间索引的 R-tree(R 树)场景。空间对象并非一维全序键,传统“前一个键到当前键”的临键区间无法精确表达矩形相交、包含等空间关系;InnoDB(事务存储引擎)因此可以用 Predicate Lock(谓词锁)表达查询谓词的几何范围。不要把它当作普通 B+Tree(多路平衡树)索引范围锁的别名,也不要以为任何带 where(条件子句)的普通表查询都会生成谓词锁。空间锁冲突、索引可用性和具体函数均应在目标版本和空间参考系下验证。
| 场景 | 一维 B+Tree(多路平衡树)索引 | 空间 R-tree(R 树)索引 |
|---|---|---|
| 键关系 | 可按单一顺序排列 | 以最小外接矩形关系判断 |
| 典型保护 | 记录、间隙、临键 | Predicate Lock(谓词锁) |
| 查询例子 | 订单号范围 | 围栏区域相交 |
| 误区 | 以为逻辑条件就是锁 | 以为仍可画成单一数轴 |
flowchart TD
G["空间范围谓词"] --> R["R-tree(R 树)索引"] --> P["Predicate Lock(谓词锁)"]
B["数值/字符串范围"] --> T["B+Tree(多路平衡树)索引"] --> N["记录/间隙/临键锁"]
P -.不能简化为.-> N热门面试题
问题(基础题):什么是 Predicate Lock(谓词锁),何时需要提到它?
- 考点:空间索引并发控制边界。
- 回答思路:先限定空间索引,再说明保护的是查询谓词范围。
- 详细答案:它用于空间索引相关的谓词保护,解决二维或多维对象不能映射成单一临键区间的问题。普通订单、库存、支付表的 B+Tree(多路平衡树)索引讨论记录锁、间隙锁和临键锁即可。
- 进阶追问:IoT(物联网)报警地图查询会自动安全吗?
- 进阶回答:不会;空间锁只是一层并发控制,报警去重仍需事件标识、时间窗、状态机、消息幂等和告警聚合策略。
问题(原理题):为什么不能用一维间隙锁完整描述空间相交?
- 考点:几何谓词与全序索引差异。
- 回答思路:说明矩形在二维平面没有唯一前驱后继。
- 详细答案:B+Tree(多路平衡树)键可按大小排列并定义相邻间隙,空间对象可能在 x(横轴)方向重叠而 y(纵轴)方向分离,或反之;谓词保护需要表达“与区域相交”的关系,不能可靠压缩成一个数轴开闭区间。
- 进阶追问:空间索引是否适合高频单点状态更新?
- 进阶回答:取决于查询模式;若核心是按设备标识更新状态,普通唯一索引通常更直接,空间索引适合地理范围检索并需评估维护成本。
问题(项目题):IoT(物联网)报警风暴治理怎样避免把空间锁当去重方案?
- 考点:数据库锁与业务幂等分层。
- 回答思路:说明同一设备、规则、时间窗的幂等键。
- 详细答案:以设备、规则版本和时间桶建立幂等或聚合键,用条件更新推进告警状态;空间索引只帮助查询区域影响面。报警风暴时还要在消息入口限流、合并、延迟确认,数据库锁不能承载海量重复事件的全部削峰职责。
- 进阶追问:锁等待升高时先扩数据库吗?
- 进阶回答:先识别热点键、长事务、锁范围和重试风暴;横向扩容不能消除同一聚合键上的写串行。
8. RC(读已提交)与 RR(可重复读)下的锁区间判定法
锁区间必须按“先看访问索引,再看是否唯一等值、是否命中、是否范围、最后看隔离级别”的顺序推导。RR(可重复读)下,范围当前读通常要用临键或间隙保护扫描范围;RC(读已提交)下,DML(数据操纵语言)对不匹配行可较早释放记录锁,通常不依赖普通间隙锁防幻读,但重复键检查、外键检查等例外仍可能持有间隙锁。下面的表是面试推导框架,不是脱离执行计划的逐版本锁位图;遇到联合索引、索引下推、覆盖索引和优化器改道必须按实际扫描结果复核。
| 条件与索引 | RR(可重复读)锁定读/写入 | RC(读已提交)锁定读/写入 | 必须补问 |
|---|---|---|---|
| 主键等值且存在 | 记录锁为主 | 记录锁为主 | 是否还更新二级索引 |
| 唯一索引等值且存在 | 精确唯一记录为主 | 精确唯一记录为主 | 是否是唯一完整键 |
| 唯一索引等值且不存在 | 目标间隙可能受保护 | 通常不作范围幻读保护,例外核实 | 是否有重复键/外键检查 |
| 普通索引等值 | 同值组及边界可能临键保护 | 记录锁为主、范围缩小 | 重复值与扫描终点 |
| 索引范围 | 扫描记录及间隙/临键 | 扫描命中记录为主 | 上下界、是否回表 |
| 无索引谓词 | 扫描聚簇索引范围扩大 | 同样可能扫描大量记录 | 实际执行计划 |
flowchart TD
A["当前读 SQL(结构化查询语言)"] --> B{"有可用索引?"}
B -- 有 --> C{"唯一完整等值且命中?"}
C -- 是 --> D["记录锁为主"]
C -- 否 --> E{"范围/重复键/不存在?"}
E --> F["RR(可重复读)重点检查间隙/临键"]
E --> G["RC(读已提交)重点检查记录与例外"]
B -- 无 --> H["聚簇索引扫描,受影响对象扩大"]热门面试题
问题(基础题):RC(读已提交)和 RR(可重复读)最大的锁差异是什么?
- 考点:范围幻读保护与锁释放时机。
- 回答思路:限定为当前读/写入,再说明不能忽略例外。
- 详细答案:RR(可重复读)更强调以范围锁保护当前读谓词的插入边界;RC(读已提交)通常减少普通间隙锁,降低范围阻塞,并让不匹配记录更早释放。但唯一键、外键检查等路径仍可能使用间隙相关保护。
- 进阶追问:库存扣减切到 RC(读已提交)会超卖吗?
- 进阶回答:若使用同一条带
available_qty >= ?(可用库存大于等于请求数)的条件更新,以影响行数裁决,正确性不依赖 RR(可重复读)的范围防幻读;仍须压测死锁、重复请求和业务状态。
问题(原理题):为何“唯一索引等值”还要强调“完整且命中”?
- 考点:唯一性、空值和不存在键边界。
- 回答思路:说明只在确实定位一条索引记录时才可缩小范围。
- 详细答案:唯一索引只有用完整唯一键并命中已有记录时,才能直接落到一个确定记录。若查询值不存在,需考虑目标间隙;若复合唯一键只给前缀,依旧可能扫描多条;若列允许 NULL(空值),唯一约束的语义也要单独确认。
- 进阶追问:为什么要看
explain(执行计划)? - 进阶回答:SQL(结构化查询语言)写成等值并不保证实际走预期索引;执行计划和真实参数决定扫描路径,继而影响锁对象与等待面。
问题(项目题):订单取消与发货并发,如何选择锁与状态条件?
- 考点:状态机前置条件与热点行。
- 回答思路:用订单主键精确更新并限制合法状态迁移。
- 详细答案:取消与发货均以订单主键作条件更新,例如只允许从各自合法前置状态迁移,影响行数为零则读取当前状态返回幂等结果或冲突结果。主键定位把锁集中到单订单;跨订单批处理必须统一排序,不能依赖范围锁“碰巧”保持状态正确。
- 进阶追问:状态表外还有库存怎么办?
- 进阶回答:在本地事务中按固定顺序更新订单与库存,或通过可靠事件和补偿实现跨边界一致;每一步要有可对账的业务流水。
9. 锁等待、超时与等待链路
锁等待是某个事务请求的锁与另一个未释放锁不兼容,但等待关系尚未形成环。它可能在持有者提交后自然恢复,也可能达到 innodb_lock_wait_timeout(锁等待超时)后由等待者报错退出。超时不是死锁检测失败的同义词:死锁是存在闭环时由检测器主动选择牺牲者;单向等待没有环,只能等待、超时、人工中断或持有者结束。应用对超时与死锁都应保留 SQL(结构化查询语言)、绑定参数、事务年龄、影响行数和幂等键,并按业务可重入性有限退避重试。
| 结果 | 等待关系 | 谁回滚 | 首要治理 |
|---|---|---|---|
| 正常唤醒 | 单向边最终解除 | 无 | 缩短持锁时间 |
| 锁等待超时 | 无环但超过阈值 | 等待者的当前语句/事务按配置处理 | 定位长事务与热点 |
| 死锁 | 有向图存在环 | 检测器选择一个牺牲者 | 统一顺序、缩小事务 |
| 人工中断 | 运维终止会话 | 被中断事务 | 保全证据、谨慎止血 |
sequenceDiagram
participant T1 as T1(事务)
participant R as 库存记录
participant T2 as T2(事务)
T1->>R: X(排他锁)并执行条件更新
T2->>R: 请求 X(排他锁)
R-->>T2: 锁等待(单向)
alt T1(事务)提交
T1->>R: COMMIT(提交)释放锁
R-->>T2: 唤醒并重新判断条件
else 超过超时
R-->>T2: Lock wait timeout(锁等待超时)
end热门面试题
问题(基础题):锁等待与死锁有什么根本区别?
- 考点:等待图是否有环。
- 回答思路:用一条边和两条闭环边对比。
- 详细答案:锁等待是 T2(事务)等待 T1(事务)释放资源,若 T1(事务)能完成就会恢复;死锁是 T1(事务)等 T2(事务)而 T2(事务)又等 T1(事务),没有外部动作无法自行推进。前者可能超时,后者由检测器尽快打破。
- 进阶追问:锁等待超时是否一定要重试?
- 进阶回答:只对幂等、可重入并且仍符合业务时限的动作做有限重试;非幂等扣款、外部通知必须先查询已有状态,不能盲发第二次。
问题(原理题):为什么被唤醒的库存更新还要重新检查条件?
- 考点:当前读与等待后可见最新值。
- 回答思路:说明先到事务可能已经改变库存。
- 详细答案:T2(事务)等待期间 T1(事务)可能已将
available_qty(可用库存)从 12 改为 4。T2(事务)获得 X(排他锁)后必须在最新记录上重新评估available_qty >= requested_qty(可用库存大于等于请求数量),不能沿用等待前的普通查询结果。 - 进阶追问:这是否需要应用显式再查一次?
- 进阶回答:条件写入语句本身完成重新检查,应用只根据影响行数判断成功或库存不足,避免额外读写窗口。
问题(项目题):库存热点导致大量锁等待时,如何止血?
- 考点:证据、削峰和正确性。
- 回答思路:先看持锁者和热点键,再限制放大流量。
- 详细答案:先抓取活跃事务、锁等待链、热点 SKU(库存单位)、请求重试率和连接池排队;限流或按 SKU(库存单位)排队,关闭无限重试,缩短事务内远程调用。数据库侧不靠盲目调大超时掩盖问题,后续以条件更新影响行数和库存流水对账验证没有超卖。
- 进阶追问:是否应该把该 SKU(库存单位)迁到新库?
- 进阶回答:若根因是单键写串行,迁库不改变该键的临界区;可考虑预热、分段库存、排队和业务拆分,但要重新证明守恒关系。
10. 死锁检测、wait-for graph(等待图)与牺牲者选择
InnoDB(事务存储引擎)在默认 innodb_deadlock_detect=ON(死锁检测开启)时维护 wait-for graph(等待图):节点是事务,有向边 T1 → T2 表示 T1(事务)等待 T2(事务)持有的冲突锁。发现环后,引擎选择回滚成本较小的事务作为牺牲者,以让其他事务继续;这不是按“谁先来谁倒霉”或“谁 SQL(结构化查询语言)短谁回滚”的简单规则。极高并发、单热点写入下检测本身可能产生 CPU(中央处理器)成本;关闭检测后则依赖锁等待超时打破冲突,通常只在明确的队列化或分片方案下审慎评估。
| 等待图元素 | 含义 | 诊断价值 |
|---|---|---|
| 节点 | 活跃事务 | 看事务年龄、SQL(结构化查询语言)与业务键 |
| 有向边 | 请求者等待持有者 | 找阻塞根与闭环 |
| 环 | 无法自行前进的依赖 | 证明死锁而非慢 SQL(结构化查询语言) |
| 牺牲者 | 被回滚的事务 | 应用需要识别并有限重试 |
flowchart LR
T1["T1(事务)持有订单 A"] -->|等待库存 B| T2["T2(事务)持有库存 B"]
T2 -->|等待订单 A| T1
D["deadlock detector(死锁检测器)"] --> V["选择回滚成本较小的牺牲者"]
V --> R["释放其锁,另一个事务继续"]热门面试题
问题(基础题):wait-for graph(等待图)怎样判断死锁?
- 考点:有向图环检测。
- 回答思路:定义节点与边,再说明闭环。
- 详细答案:每个活跃事务是节点;当 T1(事务)等待 T2(事务)持有的锁时画 T1→T2。沿边回到起始节点就形成环,说明这些事务互相等待且无法仅靠彼此提交解除,引擎必须回滚一个事务。
- 进阶追问:三个事务也会死锁吗?
- 进阶回答:会;T1→T2→T3→T1 同样是环。生产排查不应只找两条 SQL(结构化查询语言),而要还原完整资源顺序。
问题(原理题):为什么死锁检测器不保证“最小业务损失”?
- 考点:数据库可见成本与业务语义差异。
- 回答思路:区分回滚行数等存储成本和业务价值。
- 详细答案:引擎能估算事务锁、修改等技术成本,却不知道一笔支付确认和一条可重放的异步任务哪个业务损失更大。因此应用必须把操作设计为幂等、可重试和可补偿,不能把业务恢复策略交给牺牲者选择算法。
- 进阶追问:关闭检测能消灭死锁吗?
- 进阶回答:不能;等待环仍存在,只是通常由超时更晚地打断,延迟和资源占用可能更差。
问题(项目题):订单与库存更新为什么必须统一顺序?
- 考点:多资源加锁顺序。
- 回答思路:对比 T1(事务)先订单后库存与 T2(事务)反序。
- 详细答案:若取消订单先锁订单再回补库存,而发货先锁库存再改订单,就可能形成等待环。规定所有路径按相同实体类型与主键排序加锁,并把远程调用移出事务,可显著降低死锁;仍需捕获死锁并按幂等键重试。
- 进阶追问:统一顺序是否保证零死锁?
- 进阶回答:不能保证,二级索引维护、范围锁、外键和隐式访问仍可能引入资源;但它是最有效、最可审计的基础治理。
11. SHOW ENGINE INNODB STATUS(显示引擎状态)与 performance_schema(性能模式)诊断
SHOW ENGINE INNODB STATUS(显示引擎状态)中的 LATEST DETECTED DEADLOCK(最近检测到的死锁)可快速查看最近一次死锁的事务、索引、锁模式与 SQL(结构化查询语言),但它只保存最近事件,不能代替持续采集。MySQL(关系型数据库)8.0/8.4 优先使用 performance_schema.data_locks(性能模式数据锁)、performance_schema.data_lock_waits(性能模式数据锁等待)、performance_schema.threads(性能模式线程)与 performance_schema.events_statements_current(性能模式当前语句事件)还原实时链路;5.7 常见入口还包括 information_schema.innodb_trx(信息模式事务)、innodb_locks(信息模式锁)和 innodb_lock_waits(信息模式锁等待)。视图名称与字段因版本变化,命令必须在演练库先验证。
| 证据源 | 适合回答 | 局限 |
|---|---|---|
SHOW ENGINE INNODB STATUS(显示引擎状态) | 最近死锁如何形成 | 仅最近一次,文本需解析 |
data_locks(数据锁) | 当前持有哪些记录/范围锁 | 需要关联线程与事务 |
data_lock_waits(数据锁等待) | 谁等待谁 | 瞬时数据,需及时采集 |
innodb_trx(信息模式事务) | 长事务与事务状态 | 5.7/8.0 视图习惯不同 |
| 业务日志与链路 | 幂等键、参数、影响行数 | 无法单独证明锁模式 |
flowchart TD
A["告警:延迟/超时"] --> B["抓活跃事务与 data_lock_waits(数据锁等待)"]
B --> C["关联 data_locks(数据锁)和当前 SQL(结构化查询语言)"]
C --> D{"存在环?"}
D -- 是 --> E["读取最近死锁 + 应用错误码"]
D -- 否 --> F["定位最老持锁事务/MDL(元数据锁)"]
E --> G["固定顺序、缩小锁范围、幂等重试"]
F --> G热门面试题
问题(基础题):线上遇到锁等待,第一批证据应包含什么?
- 考点:可复现等待链。
- 回答思路:事务、锁、SQL(结构化查询语言)、时间与业务键同时采集。
- 详细答案:至少采集等待者和持有者的事务标识、开始时间、当前/最近 SQL(结构化查询语言)、索引与锁模式、绑定参数、连接来源、业务请求号和影响行数。只看一个慢 SQL(结构化查询语言)无法证明它就是锁根因。
- 进阶追问:为什么不只看
SHOW PROCESSLIST(显示进程列表)? - 进阶回答:它能显示会话状态但不完整呈现精确锁对象与等待关系;要和
performance_schema(性能模式)的锁视图、事务视图关联。
问题(原理题):
LATEST DETECTED DEADLOCK(最近检测到的死锁)怎样用于根因分析?- 考点:从死锁文本还原资源顺序。
- 回答思路:提取每个事务的已持有锁、待请求锁和 SQL(结构化查询语言)。
- 详细答案:将两个或多个事务的“持锁索引记录/范围”和“等待索引记录/范围”画为等待图,再对应到业务流程和参数。随后确认是否由反序更新、二级索引访问、范围扫描或外键触发,并用同数据集并发复演修复效果。
- 进阶追问:为什么截图不足以结案?
- 进阶回答:最近死锁会被下一次覆盖,且没有应用请求上下文;应持续采集指标和错误日志,将数据库事务与业务幂等键关联。
问题(项目题):支付系统如何建立锁故障可观测性?
- 考点:数据库指标与业务守恒指标联动。
- 回答思路:数据库侧看等待/死锁,业务侧看回调重试与对账。
- 详细答案:监控活跃事务时长、锁等待数与等待时间、死锁错误、MDL(元数据锁)等待、连接池排队;同时统计渠道回调重复率、流水唯一冲突、状态条件更新零行率和账实差异。告警触发时按交易号追踪事务证据,恢复后对账而非仅以错误消失判断正确。
- 进阶追问:能否记录完整 SQL(结构化查询语言)参数?
- 进阶回答:对敏感字段脱敏并限制保留期;关键是可关联的事务、请求、业务键和执行计划,而不是无边界收集数据。
12. 锁设计、事务边界与可复述项目话术
锁不是替业务做决策,而是让同一数据库边界内的决策在并发下可串行化到正确结果。库存扣减以主键或唯一业务键精确定位,并将非负判断放在条件更新;支付以渠道流水唯一约束、金额校验和状态前驱保证幂等;订单状态以有限状态机限制合法迁移;异步 Runner(执行器)领取以短事务、稳定顺序和可恢复租约控制竞争。所有方案都要考虑事务内禁止慢远程调用、减少非必要二级索引、统一资源顺序、识别可重试错误,并把数据库结果、消息投递和对账补偿连接起来。
| 场景 | 数据库内原子动作 | 锁设计 | 数据库外兜底 |
|---|---|---|---|
| 库存预占 | 条件扣减加预占流水 | 主键/唯一键精确 X(排他锁) | 幂等键、库存对账 |
| 支付回调 | 流水唯一写入加状态推进 | 唯一索引与订单主键 | 查单、补偿、资金对账 |
| 订单状态 | 前置状态条件更新 | 单订单主键锁 | 事件序号、重放防重 |
| 任务领取 | 状态加租约条件更新 | 稳定排序、短事务 | 超时回收、重复执行幂等 |
flowchart LR
A["请求号/渠道号"] --> B["唯一约束建立事实"]
B --> C["条件更新推进状态"]
C --> D["COMMIT(提交)"]
D --> E["可靠事件/异步投递"]
E --> F["对账与补偿"]
X["锁等待/死锁"] --> Y["仅幂等动作有限重试"] --> B热门面试题
问题(基础题):为什么不能把锁当成业务一致性的全部?
- 考点:数据库原子性边界。
- 回答思路:列出锁能做和不能做的事情。
- 详细答案:锁能协调同库并发访问和当前读写冲突,不能保证外部支付渠道只回调一次、消息必达、跨库资金守恒或人工误操作可恢复。因此还要有唯一约束、状态机、可靠投递、补偿和对账。
- 进阶追问:没有锁能否做幂等?
- 进阶回答:唯一约束和条件更新本身会使用数据库并发控制;关键不是应用手写互斥,而是把幂等事实与状态判断放在同一可靠边界。
问题(原理题):为什么要把远程调用移出数据库事务?
- 考点:持锁时长与不可控延迟。
- 回答思路:说明网络延迟会放大锁等待且无法由数据库回滚。
- 详细答案:事务内调用支付、物流或消息服务会在网络抖动时长期持有索引锁,造成连接堆积和死锁概率上升;即使本地回滚,远端可能已经成功。应先提交本地状态和待投递事件,再异步发送、重试与对账。
- 进阶追问:提交后发送失败怎么办?
- 进阶回答:使用事务内落库的事件表或可靠消息机制,由后台补投并以消费幂等收口,不能用长事务包住远程调用。
问题(项目题):请用一分钟讲 WMS(仓储管理系统)防超卖与锁设计。
- 考点:可复述工程闭环。
- 回答思路:背景、原子 SQL(结构化查询语言)、幂等、观测和兜底五句完成。
- 详细答案:我们把库存事实放在仓库加 SKU(库存单位)的唯一记录上,预占用带可用量条件的更新在一个短事务内完成,影响行数为一才创建预占流水;因此并发请求在同一索引记录上串行并重新校验余额。请求号有唯一约束,重复提交返回既有结果;锁等待或死锁只对幂等预占有限重试。线上关联热点 SKU(库存单位)、活跃事务、等待链和库存守恒,异常通过释放、取消和对账补偿收口。
- 进阶追问:高峰单 SKU(库存单位)成为瓶颈怎么办?
- 进阶回答:先证明热点集中于同一业务键,再采取按键排队、预热、分段库存或削峰;任何拆分都必须保持总库存守恒和可回滚对账。
9. 锁范围与并发数据演绎(14 组)
以下演绎统一假设二级索引 idx_status_created(status, created_at, id)(状态、创建时间、标识),同一索引顺序上的片段为 ('NEW',10,1)、('NEW',20,2)、('NEW',30,3)、('PAID',10,4)。区间用于训练推导;真实锁位以版本、执行计划、联合键比较和实际扫描为准。
flowchart LR
A["('NEW',10,1)"] --> B["('NEW',20,2)"] --> C["('NEW',30,3)"] --> D["('PAID',10,4)"]
X["范围当前读"] -.锁住扫描到的记录与边界间隙.-> B
X -.-> C| 编号 | 访问与隔离 | 关键锁区间/并发结果 |
|---|---|---|
| 1 | RR(可重复读),主键 id=2 命中 | 聚簇索引 [2];另一更新 id=2 等待 |
| 2 | RC(读已提交),主键 id=2 命中 | 同样以 [2] 为主;读提交后释放 |
| 3 | RR(可重复读),唯一键 request_no='P2' 命中 | 唯一二级记录与聚簇记录受保护 |
| 4 | RR(可重复读),唯一键 request_no='P15' 不存在 | 目标落点间隙可能被保护;插入 P15 等待 |
| 5 | RC(读已提交),唯一不存在键 | 通常不为范围幻读长期保护该间隙;重复键检查例外核实 |
| 6 | RR(可重复读),普通索引 status='NEW' for update(状态等值更新锁定) | 同值组和两侧扫描边界可能以临键方式保护 |
| 7 | RC(读已提交),普通索引 status='NEW' for update(状态等值更新锁定) | 命中记录锁为主;不把 RR(可重复读)范围结论照搬 |
| 8 | RR(可重复读),created_at between 10 and 30(创建时间范围) | 扫描记录加相邻边界,范围内插入可能等待 |
| 9 | RC(读已提交),同一范围更新 | 扫描命中记录为主;实际语句和例外需验证 |
| 10 | RR(可重复读),无索引 remark='x' 更新 | 聚簇索引扫描,受影响记录面扩大,不是自动表锁 |
| 11 | RR(可重复读),insert(插入)键落在 (20,30) | 先取插入意向;若已有间隙/临键锁则等待 |
| 12 | RR(可重复读),普通索引范围删除 | 删除扫描到的二级/聚簇记录,并保护相关范围 |
| 13 | 两事务反序更新订单与库存 | T1→T2、T2→T1 形成等待环,检测器回滚一方 |
| 14 | 长事务查询后 alter table(修改表结构) | MDL(元数据锁)等待;排队 DDL(数据定义语言)可阻塞后续访问 |
9.1 到 9.14 分组推演与结论
演绎 1:主键存在记录的精确冲突
库存 id=2 当前可用 12,T1(事务)执行条件扣减 8 并持有 [2] 的 X(排他锁),T2(事务)扣减 6 等待。T1(事务)提交后 T2(事务)在最新值 4 上重新判定条件失败,影响行数为零;因此最多一笔成功。这是记录锁与条件更新共同保证,而不是先读后写。
演绎 2:RC(读已提交)并不消除单记录写冲突
同一主键在 RC(读已提交)下仍只能被一个 X(排他锁)修改者持有;区别不在于后写者能并发改同一行,而在于范围锁、读视图与不匹配扫描记录的处理。切换隔离级别前必须用相同压力、参数和业务重试策略压测。
演绎 3:支付唯一键命中
T1(事务)插入渠道号 C100 后未提交,T2(事务)收到同一回调再插入 C100,在唯一索引检查路径等待。T1(事务)提交后 T2(事务)收到唯一冲突并查询既有流水;业务返回“已处理”,而不是再创建第二笔入账。
演绎 4:唯一键不存在的间隙
索引已有 P10 与 P20,T1(事务)在 RR(可重复读)下对不存在 P15 做锁定读或相关写入路径,目标是 (P10,P20) 间隙;T2(事务)插入 P15 申请插入意向锁而等待。不存在记录没有记录锁,但其插入位置仍可被保护。
演绎 5:RC(读已提交)的不存在键边界
同样的 P15 场景,RC(读已提交)下不要机械声称一定有长期间隙保护。若语句涉及唯一性或外键检查,仍可能产生必要的间隙相关锁;应抓取 data_locks(数据锁)并通过并发脚本复演,而不是以口头口诀定案。
演绎 6:普通索引等值的重复键组
status='NEW' 对应三条索引记录。RR(可重复读)下 for update(更新锁定)必须扫描该重复键组,锁可能覆盖同值记录及其边界,防止新 NEW 记录进入谓词范围。将状态字段单独建低选择性索引常使锁面和扫描量一起扩大。
演绎 7:RC(读已提交)下的任务领取
任务领取按 status='READY'(状态为就绪)扫描,在 RC(读已提交)下记录锁竞争可能低于 RR(可重复读),但不能省略稳定排序、领取标记和租约。多个消费者看到的候选集变化是允许的,前提是每条领取仍以条件更新或锁定读保证唯一归属。
演绎 8:有索引范围当前读
T1(事务)在 RR(可重复读)下扫描 created_at between 10 and 30(创建时间范围)并锁定,二级索引上命中的记录和相关边界会限制 T2(事务)插入新时间 25 的记录。范围终点、联合索引前缀和排序方向都会改变实际扫描终止点。
演绎 9:RC(读已提交)范围更新的取舍
RC(读已提交)可降低不必要范围阻塞,但报表与任务模型要接受每次读到不同已提交结果。若业务需要“本批次候选集合固定”,应使用显式领取状态、批次标识或其他设计,而不是误把 RC(读已提交)当 RR(可重复读)。
演绎 10:无索引条件像“锁住全表”
update orders set flag=1 where remark='x'(按备注更新)没有索引时要扫描聚簇索引来判断每条记录,写入路径可能锁住大量扫描命中对象并造成长时间冲突。根治是补齐能支撑访问路径的索引或改造批处理,不是期待 InnoDB(事务存储引擎)神秘地锁升级。
演绎 11:插入意向锁的可并行与阻塞
T1(事务)与 T2(事务)都向 (20,30) 插入不同键 22、25,若没有范围保护,二者插入意向锁不必互相阻塞;若 T0(事务)持有覆盖 (20,30] 的临键锁,二者都等待 T0(事务)。这解释了“插入很多但互不冲突”和“突然全等一个范围查询”两种现象。
演绎 12:范围删除与二级索引维护
按 status='CANCELLED'(状态为已取消)范围删除不仅删除聚簇记录,也要删除对应二级索引条目;大事务会持有更多锁、生成更多 undo log(回滚日志)并延迟清理。应按主键稳定分批,控制每批提交时间,并在低峰验证复制和回滚窗口。
演绎 13:订单—库存反序死锁
T1(事务)先锁订单 O1 再锁库存 S1,T2(事务)先锁库存 S1 再锁订单 O1;两者互等构成环。将所有业务路径固定为“先库存再订单,实体内按主键升序”后,死锁概率大幅下降;数据库错误仍需以请求号保证的幂等重试收口。
演绎 14:长事务—DDL(数据定义语言)—新请求的 MDL(元数据锁)链
T1(事务)开启后做查询不提交,T2(事务)申请改表在 MDL(元数据锁)队列等待,T3(事务)新来的写请求又排在 T2(事务)后。表面上 T3(事务)“卡在改表”,根因却是 T1(事务)长事务。止血要先识别最老持锁者并确认业务影响,再安排结束或等待。
flowchart TD
A["14 组演绎"] --> B["精确主键/唯一键"]
A --> C["普通索引/范围"]
A --> D["无索引/批处理"]
A --> E["死锁/MDL(元数据锁)"]
B --> F["条件更新与唯一幂等"]
C --> F
D --> F
E --> F| 演绎类别 | 你应输出的证据 | 正确结论 |
|---|---|---|
| 精确键 | 主键/唯一索引、影响行数 | 冲突集中于目标记录 |
| 范围键 | 执行计划、索引边界、隔离级别 | 记录与间隙范围由扫描决定 |
| 无索引 | 扫描行数、锁等待、耗时 | 是访问路径放大,不是锁升级 |
| 死锁/MDL(元数据锁) | 等待图、事务年龄、队列 | 找环或最老阻塞者后治理 |
10. 高频面试题与追问(28 道综合长题)
这一题库不再引入新知识点;它将前文的锁对象、索引区间、隔离级别、等待图和项目约束串成可在面试中直接复述的答案。每题先给结论,再交代前提、执行顺序、失败边界、观测证据与工程兜底。复习时请用自己的表结构和真实索引替换示例键值。
综合题 1:请完整解释 InnoDB(事务存储引擎)“行锁”锁的到底是什么
口述答案:我不会把 InnoDB(事务存储引擎)行锁解释为锁住一条抽象业务行。更准确地说,它在索引记录及索引记录之间的间隙上登记锁;聚簇索引和二级索引是实际载体。执行
update(更新)时,引擎先按执行计划扫描索引,再对扫描到的记录和在 RR(可重复读)下需要保护的间隙申请锁。主键等值命中通常很精确,普通索引范围或无索引扫描则会扩大影响面。所谓“行锁很多变成表锁”在 InnoDB(事务存储引擎)里通常是错误表述:真正发生的是索引扫描范围变大、MDL(元数据锁)阻塞、显式表锁,或业务热点使大量记录被锁。面试中我会补充二级索引更新可能既锁二级索引条目又回到聚簇索引记录,因而不能只看一个业务字段。线上排查先拿
explain(执行计划)确认访问路径,再关联performance_schema.data_locks(性能模式数据锁)和事务 SQL(结构化查询语言),而不是仅从“慢”推断为锁。库存扣减用warehouse_id + sku_id(仓库标识加库存单位)唯一键和条件更新收敛到一个记录;订单按状态低选择性索引批量更新则容易扫出大锁面。最终结论是:索引设计既决定查询成本,也决定并发冲突边界。
综合题 2:记录锁、间隙锁、临键锁和插入意向锁如何用一个例子讲清
口述答案:假设二级索引键按
10、20、30排列。记录锁是锁住[20],保护已有键 20;间隙锁是锁住(10,20),其中没有记录,但禁止在这个空档插入;临键锁是(10,20],即前面的间隙加当前记录,常见于 RR(可重复读)范围当前读;插入意向锁则是事务准备向(10,20)插入 15 时登记的意图。它不表示插入已经成功,也不是把整个间隙独占起来。这四个概念的工程价值在于解释两类现象:两个事务向同一个大间隙插入不同键,通常不必因插入意向锁互相等待;但若第三个事务以范围锁持有
(10,20],两个插入都会等待它。临键锁防护的是索引区间,不能简单念成“锁住所有符合业务条件的行”。在支付流水唯一键上,目标不存在时也可能因所在间隙而等待;在 WMS(仓储管理系统)库存主键命中场景,通常只需记录锁,正确性主要靠available_qty >= requested_qty(可用库存大于等于请求数量)条件。排查时以实际索引键和区间还原,不能只看表名。
综合题 3:RR(可重复读)下范围当前读为什么能防部分幻读
口述答案:先区分一致性读和当前读。普通
select(查询)在 RR(可重复读)下通过 Read View(读视图)复用快照,使同一事务的快照结果稳定;select ... for update(查询后更新锁定)、update(更新)和delete(删除)属于当前读,需要基于最新记录做写入裁决。为避免另一个事务在当前读的扫描谓词内插入新索引键,InnoDB(事务存储引擎)会对扫描到的记录及必要边界使用记录、间隙或临键保护。例如普通索引上扫描
status='NEW'(状态为新建)时,重复值组和其边界是锁推导的重点;新插入一条NEW记录可能会落入已保护区间而等待。因此它防的是当前读范围内并发插入改变后续锁定读目标的情形。边界在于:真实范围取决于索引、联合键、执行计划和扫描终点;快照读稳定也不代表写入可以按旧库存裁决。项目里库存防超卖使用条件更新,订单任务领取则根据是否允许跳过选择锁定读或skip locked(跳过已锁定记录),都不能只背“RR(可重复读)防幻读”这一句。
综合题 4:RC(读已提交)和 RR(可重复读)怎样选择,锁有什么差异
口述答案:我先不把隔离级别当性能开关,而是问业务是否需要同一事务内一致性读保持同一快照、范围当前读是否要阻止并发插入、以及事务是否很短。RR(可重复读)是 MySQL(关系型数据库)常用默认级别,一致性读复用读视图,并对锁定读范围使用更强的间隙或临键保护;RC(读已提交)让每次一致性读看最新已提交版本,通常减少普通范围间隙保护并可更早释放不匹配记录,从而降低某些范围阻塞。
但 RC(读已提交)不是“没有间隙锁”,重复键检查和外键检查等例外仍需实测;RR(可重复读)也不是“没有任何幻读语义边界”。库存扣减若使用单条条件更新并用影响行数裁决,正确性不依赖用 RR(可重复读)锁住一个库存范围;任务扫描若允许候选集变化,RC(读已提交)可能更合适,但需用领取状态和租约收敛。切换前我会用生产参数、真实索引和并发脚本验证
data_locks(数据锁)、吞吐、死锁率和业务对账结果,而不是根据经验口号改全局配置。
综合题 5:为什么主键、唯一索引、普通索引和无索引条件的锁面不同
口述答案:锁面本质由“为了执行当前读,引擎需要扫描哪些索引条目”决定。主键是聚簇索引,完整等值命中一个存在键时可直接定位该记录;完整唯一二级索引命中时也能精确定位唯一条目,但若最终修改行内容还会访问聚簇记录。普通索引允许重复键,等值条件需要扫描同值组,并在 RR(可重复读)锁定读中关注前后边界;范围谓词扫描从起点到终点,锁面随扫描扩大。
没有合适索引时,引擎只能扫描聚簇索引大量记录判断条件,因而持锁、执行时间、undo log(回滚日志)和等待面都可能扩大。这看起来像表被锁住,实际是访问路径没有收敛,而非行锁自动升级。支付回调用渠道流水唯一索引,既让重复请求在唯一边界被裁决,也让查询定位精确;订单后台按无索引备注更新则必须补索引或按主键分批。诊断要看实际
explain(执行计划)、行数与锁证据,不能因代码写了等号就假定一定是点查。
综合题 6:唯一索引查不存在记录,为什么还会产生等待
口述答案:不存在记录不能加记录锁,但它有一个确定的“若要插入会放在哪里”的索引间隙。假设唯一索引已有
P10和P20,目标P15不存在;RR(可重复读)下的锁定读或相关写入路径为了保持当前读谓词稳定,可能在(P10,P20)这个间隙上建立保护。其他事务若插入P15,需先申请插入意向锁,发现目标间隙被覆盖就等待。这样解释比“查不到就不加锁”准确。需要同时说清边界:普通一致性读主要使用 MVCC(多版本并发控制)而非范围锁;RC(读已提交)通常不为了普通范围幻读长期保留间隙保护,但重复键、外键等检查仍可能有间隙相关锁。支付幂等设计中,最佳实践不是先锁定查询再决定插入,而是让渠道流水唯一约束作为事实裁决;遇到重复键后读取已有结果。线上出现“插入不存在值却等待”时,抓取前后索引键、持有事务和隔离级别,才能证明到底是间隙、唯一检查还是别的长事务。
综合题 7:插入意向锁会不会导致插入之间互相阻塞
口述答案:插入意向锁存在的目的正是让引擎区分多个事务在同一大间隙不同位置插入的情况。比如索引相邻键是 20 和 30,T1(事务)要插 22,T2(事务)要插 25;若没有其他范围保护,两者的插入意向并不必然冲突,随后各自插入不同记录即可并发。它不是一个把整个间隙排他的普通锁。
但若 T0(事务)在 RR(可重复读)下以范围当前读持有覆盖
(20,30]的临键锁,T1(事务)与 T2(事务)都会因待插入位置被保护而等待。这是订单调度表常见现象:一个长事务按时间范围for update(更新锁定)扫描后,新任务写入同一时间段都变慢。处理不应粗暴取消所有锁,而是缩短领取事务、按稳定索引排序、减小扫描批次,并判断任务模型是否可使用 RC(读已提交)或skip locked(跳过已锁定记录)。最终要以任务领取唯一性、超时回收和重复执行幂等验证正确性。
综合题 8:AUTO-INC(自增)锁为什么不等同于表锁,也不能做业务排序
口述答案:AUTO-INC(自增)锁的职责是给带 AUTO_INCREMENT(自增列)的插入分配编号,它和记录锁的对象、持有时长都不同。
innodb_autoinc_lock_mode(自增锁模式)决定传统、连续或交错的分配方式:简单插入常可短暂分配连续区间,行数不确定的批量插入更需要协调,交错模式能提高并发但不保证单语句编号完全连续。回滚、预分配和并发也会造成编号空洞。所以订单、支付流水的内部自增主键只能作物理定位,不能作为“无空洞的业务流水”或可靠发生顺序。支付幂等应由渠道交易号和请求号唯一约束承担;分库路由需有独立全局标识或号段方案;按业务时间排序要用显式时间、事件序号或状态版本。若批量导入时出现等待,我会检查自增模式、语句类型、复制方式、批次大小和事务时长,而不是误诊为某条库存记录的 X(排他锁)冲突。任何调参都要验证复制正确性和灾难恢复路径。
综合题 9:请解释 MDL(元数据锁)导致“改表卡住后全站查询也卡住”的过程
口述答案:MDL(元数据锁)保护的是表定义。T1(事务)开启后访问订单表并长期不提交,即使只是普通查询,也可能在事务期间持有共享元数据锁;T2(事务)执行
alter table(修改表结构)需要更强元数据锁,因不兼容而排队;为了避免 DDL(数据定义语言)永远饥饿,之后到来的 T3(事务)查询或写入可能排在等待中的 T2(事务)后面。于是表面看到“新请求被改表阻塞”,真正根因却是最早的长事务。这与 InnoDB(事务存储引擎)记录/间隙锁不同,排查同时看活动事务、连接状态与元数据锁队列。止血前先保留 T1(事务)的 SQL(结构化查询语言)、起始时间、来源和业务请求,再与业务确认结束异常事务或让其正常完成;仅杀掉 DDL(数据定义语言)可缓解队列却不治理长事务。Online DDL(在线数据定义语言)也不意味着没有最终元数据锁窗口。订单大表加索引要在变更前清理长事务,控制锁等待阈值,有停止方案,并在演练环境验证真实版本能力。
综合题 10:Predicate Lock(谓词锁)与临键锁的边界是什么
口述答案:临键锁建立在 B+Tree(多路平衡树)的一维全序键上,可以用“前一个索引记录到当前记录”的区间描述。空间索引使用 R-tree(R 树)表达矩形的包含或相交关系,二维对象没有可直接映射的一维唯一前驱和后继;因此 InnoDB(事务存储引擎)在空间索引相关场景可使用 Predicate Lock(谓词锁)保护查询的几何谓词范围。两者都与并发插入边界有关,但不能互相替代。
IoT(物联网)报警地图的“区域内设备”查询可利用空间索引缩小读取范围,然而报警去重不能靠谓词锁:同一设备、规则版本与时间窗仍需要幂等键、状态机和聚合规则。面试里应明确普通库存、支付、订单表的主键/二级索引锁问题通常不必引入谓词锁;反过来,不能把空间相交硬画成
(10,20]。如果系统确实使用空间函数,还要确认索引可用性、坐标系、具体版本和真实锁证据。
综合题 11:一次库存扣减怎样从 SQL(结构化查询语言)到锁、幂等和对账形成闭环
口述答案:库存表以
warehouse_id + sku_id(仓库标识加库存单位)作为唯一定位键,预占使用一条带非负条件的更新,例如扣减可用量并增加预占量,条件为可用量不少于申请量。T1(事务)先在目标索引记录取得 X(排他锁),T2(事务)对同一 SKU(库存单位)等待;T1(事务)提交后,T2(事务)以最新库存重新检查条件,影响行数为零便失败。因此总量 12 同时预占 8 与 6 时最多一笔成功,数据库不需要在应用内先查后算。但锁只解决同库并发。每个预占请求还要有唯一请求号或预占单号,重复调用返回同一结果;取消、超时释放必须按预占单状态做条件更新,避免多次回补;库存流水与聚合库存定期对账。锁等待和死锁只对这类幂等动作有限退避重试,超过阈值进入补偿队列。监控同时看热点 SKU(库存单位)、影响行数、唯一冲突、锁等待、活跃事务与库存守恒,才能证明“没有超卖”不是偶然。
综合题 12:支付回调重复、并发与死锁如何一起处理
口述答案:支付回调的第一道边界是渠道交易号唯一约束:首次回调插入支付流水,重复回调要么因唯一冲突读取既有流水,要么命中既有流水直接返回幂等成功。第二道边界是订单状态与金额条件更新,例如仅允许
PAYING(支付中)到PAID(已支付)的合法转移,并校验金额、币种和商户订单号。整个数据库事务保持短小,只做流水、订单状态与待投递事件的本地写入。并发时同一交易号会在唯一索引或订单主键上串行;不同交易若多个表的访问顺序相反则可能死锁。我们统一按“支付流水—订单—账务事件”的顺序访问,捕获死锁错误后先按幂等键查询:若已经成功就返回成功,若未成功且仍可重入才有限重试。绝不在事务内调用渠道确认或消息服务;提交后由可靠事件投递,失败靠重试、查单和资金对账修复。排查把数据库死锁文本与渠道流水号、状态机迁移和账实差异关联,避免只修一条 SQL(结构化查询语言)。
综合题 13:订单状态反序更新为何会死锁,怎样设计固定顺序
口述答案:死锁的最常见业务根因不是一条 SQL(结构化查询语言)写错,而是多个事务对多个资源的访问顺序不一致。比如发货流程先锁库存 S1 再锁订单 O1;取消流程先锁订单 O1 再锁库存 S1。并发时 T1(事务)拿到 S1 等 O1,T2(事务)拿到 O1 等 S1,wait-for graph(等待图)形成闭环,InnoDB(事务存储引擎)只能回滚一个事务。
解决先制定可审计的资源顺序,例如所有流程先库存后订单,多个库存记录按仓库、SKU(库存单位)升序,多个订单按主键升序;将查物流、发消息等慢操作放到提交后。代码层把顺序封装在统一领域服务,不能让不同接口各自拼 SQL(结构化查询语言)。即使做了固定顺序,二级索引维护、范围锁、外键和临时批处理仍可能引发死锁,所以应用要把死锁视为正常可恢复错误:仅重试幂等操作,记录请求号和尝试次数,并用死锁日志持续检验是否还有新的资源环。
综合题 14:SHOW ENGINE INNODB STATUS(显示引擎状态)和 performance_schema(性能模式)怎样配合定位问题
口述答案:我把两者分工:
SHOW ENGINE INNODB STATUS(显示引擎状态)的LATEST DETECTED DEADLOCK(最近检测到的死锁)适合快速读最近一次死锁文本,提取每个事务持有什么、在等什么、执行了哪条 SQL(结构化查询语言);performance_schema(性能模式)更适合实时观察,用data_lock_waits(数据锁等待)连接等待者与持有者,用data_locks(数据锁)看锁对象,再关联线程、当前语句和事务年龄。MySQL(关系型数据库)5.7 的information_schema(信息模式)视图习惯与 8.0/8.4 不同,脚本要按版本适配。实战步骤是先保全正在发生的链路,再看最近死锁历史:采集连接、事务开始时间、SQL(结构化查询语言)、绑定参数、执行计划、锁索引、业务请求号和影响行数;判断是单向等待、等待环还是 MDL(元数据锁)队列;最后针对索引、事务边界、加锁顺序或热点做小范围修复,并用同量级并发复演。只截取一段状态文本不能证明修复有效,因为它会被后续死锁覆盖,且缺少应用层业务语义。
综合题 15:锁等待超时应该怎样处理,为什么不能一味增大超时
口述答案:
innodb_lock_wait_timeout(锁等待超时)处理的是没有被检测为死锁的等待,例如 T2(事务)单向等待 T1(事务)很久。增大超时只会让请求、连接、内存和上游重试在系统内堆积更久,不能让持锁事务更快完成;超时太小又会使正常短暂竞争变成大量失败。因此首先要回答“谁持锁、为什么持这么久、等待是否集中于同一索引键”。治理顺序是缩短事务,移走事务内远程调用,避免大范围扫描,控制批量大小,并对热点键削峰;其次设置与接口时限一致的等待预算,避免数据库等待耗尽整个应用请求期限。应用捕获超时后不能盲目重放:库存预占和任务领取可凭幂等请求号查询状态再有限退避,支付记账则要先查渠道流水和账务事实。线上记录等待时间分位、最老事务、错误率、连接池队列和业务零行率;修复后以相同并发与故障注入验证,而不是只把告警阈值调高。
综合题 16:如何证明“无索引更新”不是 InnoDB(事务存储引擎)自动锁升级
口述答案:先用
explain(执行计划)证明谓词没有可用索引,执行器必须从聚簇索引逐条扫描;再在并发会话中抓取data_locks(数据锁)、扫描行数和持锁时间。若update orders set flag=1 where remark='x'(按备注更新)扫过大量记录,其他事务对其中任何记录或相关二级索引的修改都可能受影响,于是业务观感像整表不可写。但这是扫描和锁范围扩大,并不是“行锁到一定数量升级为表锁”。修复必须从访问路径开始:为稳定谓词设计选择性足够的索引,或先按主键分页选出小批候选再提交;同时检查索引新增后写放大、回表和统计信息。不能仅加索引就宣布成功,因为低选择性
status(状态)索引仍可能扫描很大同值组。对于历史归档,使用稳定主键窗口、短事务和限速,避免一次删数百万行造成大量锁、undo log(回滚日志)和复制压力。最终比对锁等待、扫描行数、事务时长和业务正确性。
综合题 17:普通索引等值锁定为什么可能比主键范围更难推导
口述答案:主键唯一等值通常可确定一条聚簇索引记录,而普通二级索引同一个键值可能对应许多主键。
status='NEW'(状态为新建)的for update(更新锁定)并不是锁一条“状态行”,而是沿二级索引扫描同值组;RR(可重复读)下还要关注该组前后边界,防止新NEW键插入当前读谓词。更新最终记录时又可能回到聚簇索引,因此锁对象不只一个。推导时我会列出二级索引实际键序,包括复合索引的后缀主键,说明从哪条开始、到哪条停止、哪部分是记录、哪部分是间隙。若任务表按
status + next_run_time(状态加下次执行时间)领取,应该让谓词与联合索引一致,使用稳定排序并限制批次,而不是只按低选择性状态扫描。RC(读已提交)可减少部分范围阻塞,但会改变候选集可见性,需用领取状态与租约定义正确性。通过并发插入边界时间点的压测来验证,不能凭口头区间猜测上线。
综合题 18:范围删除和大事务为什么会放大锁问题
口述答案:范围删除首先是当前读:引擎按索引扫描符合条件的二级记录和聚簇记录,再删除记录并维护全部相关二级索引。一次删除过多行会持有更多锁、更长时间,生成更多 undo log(回滚日志),回滚时也更慢;如果事务迟迟不提交,还会让并发写入、purge(清理)和复制持续受压。RR(可重复读)范围保护还可能阻止范围内的并发插入。
正确的归档方案不是一句“分批删除”,而是按稳定且有索引的主键或时间窗口选择有限行数,每批独立提交并限速;记录批次边界,避免漏删或重复;持续观察锁等待、历史版本、redo log(重做日志)压力、从库延迟和业务写延迟。若必须保留可恢复性,先迁移到归档表或使用可验证的备份链路。订单历史清理绝不能在业务高峰启动一个大事务,否则即使 CPU(中央处理器)不高,锁与日志也会让核心写入雪崩。
综合题 19:外键和重复键检查如何影响“RC(读已提交)少间隙锁”的结论
口述答案:RC(读已提交)通常减少用于范围幻读保护的间隙锁,但不能推导为数据库从不锁间隙。插入或更新唯一键时,引擎需要确保不存在并发重复值;检查外键时也需保证引用关系在并发下不会被破坏。这些路径可能需要相应的记录或间隙保护,具体锁位受版本、索引和语句路径影响。
因而设计支付流水、订单明细和库存关系时,首先保证唯一键和外键索引完整,避免检查退化成大扫描;其次不要将外键当成跨服务一致性方案,它只能保护单库关系,复杂业务状态仍需应用层状态机与对账。排障出现 RC(读已提交)下的插入等待时,先查唯一键/外键、前后索引键和持锁 SQL(结构化查询语言),不要直接判为数据库异常。面试回答的价值在于交代例外与证据,而不是把隔离级别背成绝对规则。
综合题 20:如何为异步 Runner(执行器)设计低冲突的任务领取
口述答案:任务领取的核心不是把所有待执行记录长期锁住,而是把“谁拥有执行权”写成短事务内的条件状态迁移。任务表应有支持谓词的索引,例如
status、next_run_time、id(状态、下次执行时间、标识);领取时按稳定顺序选有限数量候选,用for update skip locked(更新锁定并跳过已锁定记录)或条件更新将状态改为RUNNING(执行中)、写入领取人和租约截止时间,然后立即提交。适用前提是任务可以被其他消费者跳过,且业务能接受先领取后执行;执行失败或机器宕机由租约到期回收,执行侧按任务标识保证幂等。若任务有严格全局顺序,不能用跳过来掩盖排队,应使用单分区串行消费者或显式顺序号。RC(读已提交)与 RR(可重复读)的选择取决于候选集稳定性要求,但共同原则是小批次、短事务、索引匹配和不在锁内执行远程任务。指标要覆盖领取冲突、租约过期、重复执行和积压年龄。
综合题 21:如何判断某次慢请求是锁等待、慢 SQL(结构化查询语言)还是 MDL(元数据锁)
口述答案:我先按时间线分层取证。慢 SQL(结构化查询语言)侧看执行计划、扫描行、磁盘 I/O(输入输出)和 CPU(中央处理器);InnoDB(事务存储引擎)锁等待侧看
data_lock_waits(数据锁等待)中请求者—持有者关系、锁索引、事务年龄;MDL(元数据锁)侧看是否有等待 DDL(数据定义语言)、长事务与元数据队列。三类问题可并存,例如无索引大更新既慢又长期持锁,随后又阻塞 DDL(数据定义语言)。处理不能只挑耗时最长的一条 SQL(结构化查询语言)优化。若等待边指向一个空闲但未提交的会话,根因是事务边界;若没有等待但扫描千万行,根因是访问路径;若大量新请求在
Waiting for table metadata lock(等待表元数据锁),需追最早事务和排队 DDL(数据定义语言)。保全证据后再做止血:限制流量、暂停变更、结束已确认异常会话。恢复后以同窗口指标验证等待消失、吞吐恢复且业务账实一致。
综合题 22:死锁发生时应用重试应遵循哪些原则
口述答案:数据库检测到死锁会选择一个事务回滚,应用收到的是可预期的并发错误,不代表业务一定失败。第一原则是先按幂等键查询是否已经成功:被回滚的事务可能在其他并发路径已完成相同目标,或请求本身是重复投递。第二原则是仅对可重入、无外部不可逆副作用、仍在时效窗口内的操作做有限重试,并使用带随机抖动的退避避免所有请求同时再撞热点。
第三原则是重试不是根治。若死锁持续上升,应从死锁文本还原 wait-for graph(等待图),统一多资源加锁顺序,缩短事务,收窄范围索引,避免批量与单行操作交叉。支付扣款、外部发货等操作必须把副作用与数据库状态用可靠事件、查单和补偿衔接,不能简单重新调用外部系统。监控要记录死锁错误码、重试次数、最终成功率、业务请求号和受影响表,超过阈值降级或排队。正确目标是可恢复且死锁率可控,而不是假装它永远不会发生。
综合题 23:为什么说死锁检测器可能成为热点写入的成本
口述答案:在高度并发且大量事务争抢同一或少量记录时,新的等待请求需要参与死锁检测;等待关系很多时,维护和遍历 wait-for graph(等待图)会产生 CPU(中央处理器)开销。此时延迟升高不只来自那条热点记录的串行写,也来自大量线程同时排队、超时、重试和检测。盲目增加连接数会让等待图更大,常常更糟。
可以评估
innodb_deadlock_detect(死锁检测开关)的取舍,但关闭它并不会消除环,只会改由锁等待超时更晚地打断,适合明确按键队列化、分区化且能接受超时的少数场景。优先方案是业务削峰:按 SKU(库存单位)或订单键串行化热点、减少同键并发、使用短事务和有限队列;数据库侧保证精确索引与合理连接上限。任何改变前后都比较 CPU(中央处理器)、锁等待、超时、死锁、吞吐和库存/支付正确性,不能只看到单项 CPU(中央处理器)下降就上线。
综合题 24:支付资金一致性为什么需要“锁 + 唯一约束 + 状态机 + 对账”四层
口述答案:锁解决同一时刻同一数据库记录的并发读写冲突,例如订单状态和支付流水更新不能被两个事务同时覆盖;唯一约束解决重复回调、重复提交的事实去重;状态机解决“支付中到已支付”“已支付到退款中”等合法前驱,避免重复或乱序事件把状态改坏;对账解决数据库外部渠道、消息与宕机窗口造成的最终差异。这四层的失败模型不同,任何一层都不能替代另一层。
实现上先用渠道号唯一写流水,再以订单主键和前置状态条件更新;事务内写待投递事件,提交后异步通知。死锁或超时只对请求号幂等的本地步骤重试,渠道调用通过查单确认。对账按交易号、金额、币种、状态和时间窗找差异,形成补单、冲正或人工处理证据。面试中这样回答能表明我知道数据库锁只能覆盖本地临界区,不会将分布式资金一致性误说成一次 X(排他锁)就能解决。
综合题 25:如何通过联合索引降低订单状态查询的锁冲突
口述答案:先从业务谓词出发。若任务领取常按
status='READY' and next_run_time<=now(状态为就绪且下次执行时间到期)排序取前 N 条,索引应使等值前缀、范围条件和稳定排序尽量一致,例如status、next_run_time、id(状态、下次执行时间、标识)。这样引擎从到期范围起点扫描有限条目,锁定读和skip locked(跳过已锁定记录)只触及小批索引记录,而不是先扫全部READY(就绪)再过滤。不能机械给每个条件列都加索引:范围列之后的列能否继续用于排序和过滤要结合执行计划,二级索引还会增加写入维护。上线前用真实分布检查选择性、回表、扫描行和并发插入边界;上线后观察领取延迟、锁等待、死锁、任务饥饿和索引写放大。若同一状态本身是极热点,再好的联合索引也不能绕过同一任务或同一分区的写串行,需要分区、队列或业务拆分。
综合题 26:长事务为什么同时伤害锁、MVCC(多版本并发控制)和 DDL(数据定义语言)
口述答案:长事务若执行过修改就长时间持有记录和范围锁,直接增加其他事务等待;即使只做一致性读,旧 Read View(读视图)也会让历史版本不能及时清理,undo log(回滚日志)保留变长,读取回溯和存储压力上升;同时它可能持有 MDL(元数据锁)共享锁,让改表无法拿到排他元数据锁,并通过 DDL(数据定义语言)队列间接阻塞新请求。这三种影响需要一起监控。
根治是明确事务边界:数据库事务只覆盖必要的本地读写,禁止在其中等待用户输入、调用远程接口、批量处理长循环或打开连接后忘记提交。连接池必须在归还连接前回滚/提交并清理会话状态;报表使用独立短连接和适合的快照口径。线上按事务年龄、历史列表、锁等待、MDL(元数据锁)等待和连接来源报警,先识别异常会话再谨慎结束。任何“杀长事务”操作都要评估回滚时间和业务补偿,避免止血动作自身造成更大冲击。
综合题 27:请给出一套锁问题的线上 SOP(标准操作流程)
口述答案:第一步确认影响面:是单接口、单表、单租户还是全库延迟,并记录开始时间、错误码和发布/DDL(数据定义语言)事件。第二步保全实时证据:活跃事务及开始时间、
data_lock_waits(数据锁等待)、data_locks(数据锁)、当前 SQL(结构化查询语言)与参数、执行计划、MDL(元数据锁)队列、连接池和业务请求号。第三步画出持有者—等待者链,判断单向等待、等待环、范围扫描还是元数据阻塞。第四步止血:暂停风险 DDL(数据定义语言)、限制热点流量和无限重试;经确认后结束异常且可恢复的持锁事务,不能先随意杀掉所有等待者。第五步修复:索引收敛、缩短事务、统一加锁顺序、拆分批处理或调整领取模型。第六步验证:用原始参数和同量级并发复演,比较锁等待、死锁、事务年龄和业务守恒;最后把根因、证据、变更和回滚策略写入复盘。这个流程能避免“看到超时就调大超时”的无效动作。
综合题 28:请用两分钟串讲 InnoDB(事务存储引擎)锁与死锁,并落到项目
口述答案:我会从一句核心结论开始:InnoDB(事务存储引擎)锁的是索引记录和索引间隙,锁范围由隔离级别、当前读、索引唯一性、谓词和实际扫描终点决定。S(共享锁)与 X(排他锁)处理读写冲突,IS(意向共享锁)和 IX(意向排他锁)提供表层意向;记录锁、间隙锁、临键锁和插入意向锁解释点查、范围扫描和插入等待。RR(可重复读)的一致性读靠 MVCC(多版本并发控制),当前读靠范围锁处理幻读边界;RC(读已提交)通常缩小范围保护但有唯一和外键等例外。
然后我讲故障:单向等待会被提交、超时或人工处理解除;wait-for graph(等待图)出现环才是死锁,检测器回滚一方,应用要按幂等键有限重试。线上用
SHOW ENGINE INNODB STATUS(显示引擎状态)看最近死锁,用performance_schema(性能模式)还原实时锁链,同时排查 MDL(元数据锁)与长事务。项目上,WMS(仓储管理系统)库存用唯一键条件更新和流水对账防超卖;支付用渠道号唯一、状态机、可靠事件和资金对账;订单与库存按固定顺序加锁。最后强调锁不是全部一致性,必须和幂等、状态、补偿、观测一起设计。
11. 复习清单与自测
- 你能画出
[20]、(10,20)、(10,20]三种锁区间,并说明插入 15 为什么会等待。 - 你能按“主键/唯一/普通/无索引 × 等值/范围/不存在 × RC(读已提交)/RR(可重复读)”逐步推导,而不是背固定锁表。
- 你能解释 InnoDB(事务存储引擎)一般没有从行锁到表锁的自动锁升级。
- 你能区分锁等待、超时、死锁、MDL(元数据锁)阻塞,并列出各自第一证据。
- 你能用 wait-for graph(等待图)还原订单与库存反序更新的死锁。
- 你能给出库存扣减、支付幂等、订单状态和 Runner(执行器)领取的短事务方案。
- 你能说明
SHOW ENGINE INNODB STATUS(显示引擎状态)与performance_schema(性能模式)的版本和证据边界。 - 你能在复演环境验证锁区间图、并发脚本和修复前后的业务守恒结果。
