← 学习门户 每日面试题 学习看板

2026-09-17

MySQL 索引与事务

10 题 · 平均 2.7/5 · low 2.75 · medium 2.6 · high 3 · 第 1 轮

#1 什么是事务的ACID特性 low mysql

题目

什么是事务的ACID特性

我的回答

首先事物的 ACID 性特性就是原子性、一致性、可用性,是用来保证,其实核心是用来保证一致性的。

点评

优点:- 正确指出一致性是 ACID 的核心目标
不足:- 将"隔离性"(Isolation) 误说为"可用性"(Availability),这是 ACID 四个字母的基本错误 - 没有展开任何一个特性的具体含义

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对数据库事务四大特性的定义理解、实现机制认知,以及区分 ACID 与 CAP 中可用性(Availability)的概念边界。

**知识点拆解**:
1. **ACID 四特性与实现机制**:原子性由 undo log 保障(可回滚);一致性是最终目标(数据合法状态转换);隔离性由锁 + MVCC 实现(并发事务互不干扰);持久性由 redo log 保障(宕机可恢复)。一致性是目标,另外三个是手段。
2. **ACID 与 CAP 的区别**:ACID 的 I 是 Isolation(隔离性),CAP 的 A 是 Availability(可用性)。两者字母重叠但含义完全不同,面试中混淆即失分。
3. **MySQL InnoDB 的实现路径**:undo log(原子性)→ 业务约束 + 其他三特性(一致性)→ 锁 + MVCC + 隔离级别(隔离性)→ redo log 两阶段提交(持久性)。四者协同保证事务正确性。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:undo log 和 redo log 如何分别支撑原子性和持久性
- **广度**:CAP 理论、BASE 理论、隔离级别
- **应用**:分布式事务中 ACID 如何退化(Seata AT / TCC / 本地消息表)

**延伸递进面试题**:
1. **undo log 和 redo log 有什么区别?各自解决什么问题?**
- 解答要点:undo log 记录修改前的旧值,用于回滚(原子性)和 MVCC(快照读);redo log 记录修改后的新值,用于崩溃恢复(持久性)。undo 在表空间,redo 在 ib_logfile。
2. **两阶段提交(2PC)在 InnoDB 中怎么用的?**
- 解答要点:事务提交时 redo log 先写 prepare 状态,再写 binlog,最后 redo log 写 commit。崩溃恢复时根据 binlog 是否完整决定 redo log 的 prepare 记录是 commit 还是 rollback。
3. **分布式事务如何保证 ACID?**
- 解答要点:分布式场景下 ACID 无法强保证,退化为 BASE(基本可用、软状态、最终一致)。常见方案:2PC/XA(强一致但阻塞)、TCC(最终一致但侵入大)、本地消息表(最终一致但需对账)。

第三部分:面试者参考回答(口语化)

面试官您好,事务的 ACID 特性指的是原子性、一致性、隔离性和持久性,核心目标是保证数据的一致性。

原子性是指事务中的操作要么全部成功,要么全部回滚,MySQL 是通过 undo log 来实现的——每次修改前把旧值记录下来,如果事务失败就沿 undo log 回滚。

一致性是最终目标,事务执行前后数据都要处于合法状态,比如转账场景里 A 账户扣了钱,B 账户必须加到对应的钱。这个一致性其实是由其他三个特性来保障的。

隔离性是指多个并发事务之间互不干扰,MySQL 通过锁机制和 MVCC 来实现,不同的隔离级别对应不同的可见性规则。MySQL 默认使用可重复读级别。

持久性是指事务一旦提交,即使系统崩溃数据也不会丢失,这是通过 redo log 来保证的,崩溃恢复时根据 redo log 重做未完成的操作。

所以总结来说,一致性是目标,原子性、隔离性、持久性是手段。另外需要注意,ACID 的 I 是隔离性,和 CAP 里的可用性(A)不是一个概念,这个很容易混淆。

https://chat.deepseek.com/share/bgzvkk02xzsh7bjd82

#2 什么是最左前缀匹配?为什么要遵守? low mysql

题目

什么是最左前缀匹配?为什么要遵守?

我的回答

最左匹配本质上就是索引是从前往后建的,顺序也是从往前后排的。这个前也就是左侧,所有东西从左侧开始排,它展现的结果也就是最左匹配了,后边的匹配不到,这就是所谓最左前缀匹配的逻辑。这东西本质上是创建索引排序的规则,排序本身是需要根据字段顺序来排的,它本身排序的顺序就是从左往右一点点光键的,所以导致它只能从左往右排

点评

优点:- 抓住核心:索引按字段顺序从左往右排,只能从左往右匹配
不足:- 没有解释 B+ 树排序结构决定的物理事实 - 没有给具体查询示例 - 未提及 MySQL 8.0 索引跳跃扫描例外

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对联合索引在 B+ 树中的物理排列方式的理解,以及能否将底层数据结构映射到 SQL 查询行为。

**知识点拆解**:
1. **B+ 树中的联合索引排列规则**:联合索引 (a,b,c) 在 B+ 树中先按 a 排序,a 相同按 b 排序,b 相同按 c 排序,与字典序排序一致。WHERE 条件必须从最左侧开始,跳过中间字段则后续字段失去有序性,无法利用索引范围扫描。
2. **范围查询截断效应**:WHERE a=1 AND b>2 AND c=3 中,b 用范围条件后,不同 b 值下的 c 排序各自独立,c 无法利用索引,只能回表后过滤。
3. **MySQL 8.0 索引跳跃扫描**:当最左字段区分度极低时,MySQL 8.0 可逐个遍历最左字段值,在每个子树内利用后续字段。不是万能解,需满足低基数、无索引下推等条件。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:B+ 树键值排列规则决定索引利用方式
- **广度**:索引跳跃扫描、索引下推(ICP)、covering index
- **应用**:联合索引字段顺序设计原则

**延伸递进面试题**:
1. **联合索引 (a,b,c),WHERE b=2 AND c=3 能走索引吗?**
- 解答要点:传统规则下不能(跳过 a)。MySQL 8.0 特定条件下可用索引跳跃扫描。
2. **联合索引 (a,b,c),WHERE a=1 AND c=3 能走索引吗?**
- 解答要点:a=1 走索引,c 无法利用索引排序(b 被跳过),c 回表后过滤。
3. **联合索引字段顺序怎么设计?**
- 解答要点:高频单独查询的字段放左边;区分度高的放左边;左侧字段若为范围查询则后续字段失效。

第三部分:面试者参考回答(口语化)

面试官您好,最左前缀匹配的本质是 B+ 树中联合索引的排序规则。

联合索引比如 (a,b,c),在 B+ 树里先用 a 排序,a 相同再用 b 排序,b 也相同再用 c 排序,和字典排序一个道理。所以查询条件必须从最左边开始匹配。WHERE a=1 能走索引,WHERE a=1 AND b=2 也能走,但跳过 a 直接查 WHERE b=2 就不行了,因为所有 b 值分散在不同 a 值下面,无法利用有序性。

还有一个重要场景,WHERE a=1 AND b>2 AND c=3。b 用了范围查询,不同 b 值下面的 c 排序是独立的,c 无法继续利用索引做范围扫描,只能回表后过滤。

MySQL 8.0 有个例外叫索引跳跃扫描,最左字段区分度很低时,MySQL 可以逐个遍历最左字段的值,在每个子树内利用后续字段,但这不是通用方案。

总结来说,最左前缀匹配不是 MySQL 人为规定的规则,而是 B+ 树的排序结构决定的物理事实。

https://chat.deepseek.com/share/f59k51fog1moopg9u0

#3 什么是回表,怎么减少回表的次数? low mysql

题目

什么是回表,怎么减少回表的次数?

我的回答

所谓回表,就是使用索引,非主键索引去查询数据。查询的数据到叶子层其实是没有主数据的,这时候它的数据需要从主数据去拿,这个拿的过程就是回表。回表就是通过子索引找到主键 ID,然后再通过主键 ID 去聚簇索引里边拿到主数据,然后展现出来。想要缩减回表的话,首先可以做覆盖索引,也就是说将要展示的数据统一加到索引里边来,在索引的底层直接可以拿到整体的数据,这样可以减少回表的次数。另外就是降低查询的数据,少量的数据获取也可以降低回表,因为回表是通过数据去查主表、主键索引的位置。数据量少了,对应的回表次数也就少了。

点评

优点:- 抓住核心机制:通过二级索引取主键 ID,再回聚簇索引拿完整行 - 明确指出覆盖索引是主要优化手段,思路正确
不足:- "降低查询的数据"过于模糊,没有点出 `LIMIT + 延迟关联` 这个高频实战手法 - 未提索引下推(ICP, Index Condition Pushdown)—— MySQL 5.6+ 优化项 - 未量化回表代价:随机 IO、B+ 树二次定位,为什么这是性能杀手 - 未提回表数量 = 索引扫描到的行数,而不是最终 LIMIT 行数(常见踩坑)
补充:- 覆盖索引硬约束:SELECT 的所有列必须都包含在某个索引中(含主键) - 延迟关联(Deferred Join):`SELECT * FROM t INNER JOIN (SELECT id FROM t WHERE ... ORDER BY x LIMIT 10) tmp ON t.id = tmp.id` - 索引下推(ICP):在索引扫描阶段就过滤非索引列,减少回表次数 - 回表代价:一次回表 = 一次 B+ 树根到叶的查找,若数据未常驻缓冲池则是随机磁盘 IO

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解 InnoDB 二级索引与聚簇索引的关系,掌握减少回表的工程手法。

**知识点拆解**:
1. **什么是回表**:InnoDB 采用聚簇索引,二级索引叶子节点只存「索引列 + 主键」。查询用二级索引时,先从二级索引找到主键 ID,再拿主键回聚簇索引 B+ 树查一次完整行,这个"回头找主表"的动作叫回表。
2. **回表为什么慢**:每次回表都是一次独立的 B+ 树查找,如果行数据不在 Buffer Pool 里就是随机磁盘 IO。100 万行匹配只 LIMIT 10,MySQL 仍可能回表 100 万次(在排序/过滤前)。
3. **减少回表三板斧**:
- **覆盖索引**:SELECT 的列全放进索引,叶子节点直接返回,`EXPLAIN` 的 `Extra` 显示 `Using index`。
- **延迟关联**:先用索引表子查询拿到主键 ID 并 LIMIT,再 JOIN 主表取完整行,把回表次数压到 LIMIT 数量。
- **索引下推 ICP**:MySQL 5.6+ 特性,在索引扫描阶段就用非索引列条件过滤,减少需要回表的行数,`EXPLAIN` 显示 `Using index condition`。
4. **反模式**:把大字段(TEXT/BLOB、长 VARCHAR)放进索引;SELECT * 让覆盖索引失效;给低基数字段单独建索引导致扫描后大量回表。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:是否理解 B+ 树结构决定了回表必须二次查找
- **广度**:能否关联到覆盖索引、延迟关联、ICP、聚簇索引选择
- **应用**:能否通过 `EXPLAIN` 识别回表并给出具体的 SQL 改写方案

**延伸递进面试题**:
1. **`Extra` 列里的 `Using index` 和 `Using index condition` 有什么区别?**
- 解答要点:`Using index` 是覆盖索引,完全在二级索引内解决;`Using index condition` 是索引下推,还在索引阶段做过滤但需要回表取其他列。
2. **一条 `SELECT * FROM t WHERE a=? ORDER BY b LIMIT 10` 有索引 `(a,b)`,还会有回表吗?如何彻底消除?**
- 解答要点:有。`SELECT *` 导致覆盖失败,回表 10 次。若要彻底消除,要么建覆盖索引包含所有 SELECT 列,要么用延迟关联。
3. **为什么 InnoDB 推荐用自增主键而不是 UUID 做聚簇索引?**
- 解答要点:自增主键保证插入顺序 = 物理顺序,避免页分裂和随机 IO;UUID 打散主键,二级索引叶子存的主键无序,回表和插入都会退化。

第三部分:面试者参考回答(口语化)

面试官您好,回表是 InnoDB 的一个特有现象。InnoDB 把数据存在聚簇索引里,而二级索引的叶子节点只存"索引列 + 主键 ID",不存完整行。所以当我们用二级索引查数据时,先在二级索引的 B+ 树上定位到主键 ID,再拿这个 ID 回聚簇索引 B+ 树查一次完整行,这次回头查主表的过程就叫回表。回表贵,是因为每一次都是一次独立的 B+ 树查找,如果行数据不在 Buffer Pool 里,就是随机磁盘 IO;而且回表次数取决于索引扫描到的行数,不是最终 LIMIT 数量,一条 `LIMIT 10` 的查询可能回表几百万次。

减少回表主要有三个手段。第一是覆盖索引,把 SELECT 需要的列都放进一个索引里,这样在二级索引叶子节点就能拿到全部数据,EXPLAIN 里会显示 `Using index`,完全不需要回表。第二是延迟关联,写法是先在一个子查询里只查主键 ID,配合 WHERE 和 ORDER BY 和 LIMIT,然后再拿这批主键 JOIN 回主表拿完整行,这样回表次数就被压到 LIMIT 那个数。第三是索引下推 ICP,这是 MySQL 5.6 加的特性,对于范围查询,把非索引列的过滤条件推到索引扫描阶段做,减少需要回表的行数,EXPLAIN 里能看到 `Using index condition`。

工程上还要注意几个坑,一是不要在索引里放大字段和长字符串,会让索引膨胀;二是尽量避免 SELECT *,它会让覆盖索引失效;三是主键最好用自增整数,UUID 会让二级索引里存的主键无序,回表时随机 IO 更多。

https://chat.deepseek.com/share/5e7y27yvn1p1s5d5s1

#4 MySQL中A,B,C的联合索引,按照 AB,AC,BC查询,能走索引吗? medium mysql

题目

MySQL中A,B,C的联合索引,按照 AB,AC,BC查询,能走索引吗?

我的回答

首先联合索引 ABC,按顺序来的话,ABC 间的索引 AB 跟 AC 是可以走部分的联合索引的,但是 BC 基本上不会走到联合索引。当然也有情况可能会走,就是 A 的数据很少,并且 A 的数据很单一的情况下,比如 A 是一个状态,这时候它可能做一个索引下推,在数据内部会做优化的时候,会进行类似于 UNION 将每个数据 UNION 一遍的情况。但正常情况下,B 级的查询是不会走索引的。想要做 B 级查询,可以单独建 B 级前缀的索引。

点评

优点:- AB / AC / BC 结论基本正确:AB 完整走,AC 只走 A 段,BC 默认不走 - 能识别出 A 基数很低时可能有优化介入
不足:- 未点明核心原因:联合索引 B+ 树按 A → B → C 排序,A 值不同时下层 B 才有序,缺 A 就没法用 B 有序性 - 概念混淆:"UNION 一遍"其实是**跳跃扫描 Skip Scan**(MySQL 8.0+),不是**索引下推 ICP**;ICP 是把 WHERE 条件前推到索引扫描阶段做过滤,与"用不同 A 值循环"是两码事 - 未提 `EXPLAIN Extra` 的关键标识(`Using index for skip scan` vs `Using index condition`) - 未提 `FORCE INDEX` 陷阱:即使 force 也可能只是全索引扫描,比全表扫描还慢
补充:- AB 查询:A、B 都作为定位条件;若 WHERE 只有 AB,C 不能作为定位(除非 AB 命中很少行,剩余行 C 走过滤) - AC 查询:只有 A 段用于定位,C 是"filter",`Extra` 显示 `Using where` - BC 查询:默认走不到;MySQL 8.0+ 若 A 基数很低(如 gender、status),优化器可能触发 `Using index for skip scan`,把查询拆成多个 A=? 的子查询在索引内部完成 - Skip Scan 触发条件:A 是等值、A 基数很低、后续列也有等值条件

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解联合索引的排序规则(B+ 树复合键排序)与 MySQL 8.0 的 Skip Scan 优化边界。

**知识点拆解**:
1. **联合索引的排序规则**:`(A,B,C)` 复合键的 B+ 树按字典序排序——先按 A 排序,A 相同时再按 B 排序,B 相同时再按 C 排序。所以查询能否命中索引取决于"是否提供了前缀"。
2. **AB 查询**:`WHERE a=? AND b=?` 完整命中 A、B 两段定位。若没有 C 条件,C 不能作为定位,只能作为过滤(回表后判断)。
3. **AC 查询**:`WHERE a=? AND c=?` 只有 A 段能定位,C 段中间隔了 B,无法利用排序,C 走过滤,`Extra=Using where`。
4. **BC 查询**:`WHERE b=? AND c=?` 默认走不到索引——因为 A 未知,B 的排序在不同 A 之间不连续。**MySQL 8.0+ 例外**:若 A 基数很低(如 gender=0/1),优化器可能做 **Skip Scan**,把查询拆成 `A=0 AND B=? AND C=?` 和 `A=1 AND B=? AND C=?` 两段在索引内部完成,`Extra=Using index for skip scan`。
5. **索引下推 ICP(Index Condition Pushdown)**:这是 MySQL 5.6 引入的优化,把非索引列的 WHERE 条件前推到索引扫描阶段做过滤,减少回表次数。`Extra=Using index condition`。**它跟 Skip Scan 是两回事**——ICP 减少回表次数,Skip Scan 让原本走不到的索引能走到。
6. **EXPLAIN 识别技巧**:看 `key` 是否是目标索引、`rows` 是否显著下降、`Extra` 是否出现 `Using index`(覆盖)、`Using index condition`(ICP)、`Using index for skip scan`(Skip Scan)。

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否解释为什么联合索引必须按前缀命中(B+ 树排序本质)
- **广度**:能否区分 ICP 与 Skip Scan 这两个容易混淆的优化
- **应用**:能否通过 `EXPLAIN` 判断实际执行计划并给出改进方案

**延伸递进面试题**:
1. **`WHERE a IN (1,2,3) AND b=?` 会走联合索引吗?**
- 解答要点:会。A 是有限枚举,等价于把 A 展开成几个等值条件,B 在每个 A 值内仍然有序,优化器可以正常利用。
2. **`WHERE a>? AND b=?` 会怎样?为什么范围查询后面的列用不上?**
- 解答要点:只有 A 走索引(范围),B 用不上。原因是 A 值有多个匹配范围,每个范围内的 B 才有序,跨范围 B 就不连续了。
3. **`FORCE INDEX` 能强行让 BC 走 (A,B,C) 索引吗?会有什么问题?**
- 解答要点:能强行走,但可能变成全索引扫描,比全表扫描还慢(索引比表小不了多少,还多一次回表)。所以 FORCE INDEX 要用 `EXPLAIN` 验证。

第三部分:面试者参考回答(口语化)

面试官您好,这个问题关键要看联合索引的排序规则。`(A,B,C)` 复合索引在 B+ 树上按字典序排——先按 A 排,A 相同再按 B 排,B 相同再按 C 排。所以查询要能命中索引,就必须能提供前缀。

**AB 查询** `WHERE a=? AND b=?`:A 定位到一片区间,这片区间内 B 是有序的,所以 B 也能定位,两段都能用;如果 WHERE 没有 C,C 只能作为过滤条件在回表后判断。

**AC 查询** `WHERE a=? AND c=?`:A 定位到区间后,中间隔了一个 B 未知,C 在这个区间里不再有序,所以 C 用不上索引定位,只能走过滤。EXPLAIN 会看到 `Extra=Using where`,key_len 只覆盖 A 段。

**BC 查询** `WHERE b=? AND c=?`:默认走不到 (A,B,C) 索引。因为 A 未知,B 的有序性被 A 的分隔打散。但这里有个例外,MySQL 8.0 有个 **Skip Scan(跳跃扫描)** 优化:如果 A 的基数很低,比如是性别、状态这种只有几个值的字段,优化器会把这个查询在索引内部拆成 `A=0 AND B=? AND C=?`、`A=1 AND B=? AND C=?` 等多段分别扫描再合并。EXPLAIN 的 Extra 会显示 `Using index for skip scan`。

这里容易混淆的一个概念是 **ICP(索引下推)**。ICP 是 MySQL 5.6 加的优化,把 WHERE 里非索引列的条件前推到索引扫描阶段做过滤,减少回表次数,Extra 显示 `Using index condition`。ICP 不会让走不到的索引变走得到,它只是减少回表。Skip Scan 才是让原本用不到前缀列的查询也能用上索引。

所以工程实践上,如果有明确的 BC 查询模式,还是单独建 `(B,C)` 索引更稳妥,不要指望 Skip Scan 兜底——它的触发条件比较苛刻,A 基数低是硬要求。另外也不要用 `FORCE INDEX` 硬推,因为可能退化成全索引扫描,比全表扫描还慢。

https://chat.deepseek.com/share/6snsbux2u1oe10v9gu

#5 MySQL中索引失效的问题如何排查? medium mysql

题目

MySQL中索引失效的问题如何排查?

我的回答

索引失效,SQL,MySQL 索引失效。失效的话,首先你要加了索引,对吧?MySQL 里边这个执行的 SQL 语句,使用的查询记录中使用的索引的字段。然后我们看一下执行计划,并没有走这个索引字段。然后我们我们分析可能的情况。首先走索引需要它目标值匹配,也就是说你使用的查询方式跟你所走的索引能匹配的上。首先是字段类型,如果是类似于 char 跟 int 类型去匹配,这时候可能就会没有走你对应的设置的索引。另外就是是否加了特殊的处理,比如说字段上用了计算值,比如说成本,你比较的时候使用了加几,在成本的字段上加了多少,然后去算一个值。另外就是你做了一些特殊处理,比如截取,最终导致加了一个函数,导致你的对你的设置索引的字段出现了特殊处理,使得它不属于核心。另外就是可能说你使用了索引,但是底侧你使用了非索引字段去排序,最终致它觉得优化器觉得你这使用的索引不合理,或者是说成本太高了,反而它去直接使用全表而不走你的索引,这时候可能要做联合索引去处理相关的问题。

点评

优点:- 有基本排查流程:先看执行计划确认未走索引,再分析原因 - 抓到几个高频场景:隐式类型转换、字段上施加函数/计算、ORDER BY 非索引列
不足:- **未提关键排查工具**:`EXPLAIN FORMAT=JSON` 的 `missing_covering_index` / `attached_condition`、`sys.schema_index_statistics` 的 `rows_selected/rows_examined` 比、`performance_schema.table_io_waits_summary_by_index_usage` - **未列出高频失效清单**:`LIKE '%xx'` 左模糊、`OR` 拼接非索引列、`NOT IN`、`!=`、`< >`、`IS NOT NULL` - **未提排序/分组失效**:`ORDER BY` 与 `GROUP BY` 走的是不同索引时的 `filesort` - "优化器觉得不合理"过于模糊,未说明具体判断依据(`cost`、`rows`、`key_len`) - 未提 `ANALYZE TABLE` 更新统计信息、`FORCE INDEX` 强制走索引、`optimizer_switch` 关闭 `index_merge` 等兜底手段 - 未提 MySQL 8.0 的 `EXPLAIN ANALYZE` 真实执行代价
补充:- 排查三步走:`EXPLAIN` 看 `key=NULL` → `EXPLAIN FORMAT=JSON` 看 `cost_info` 与 `missing_covering_index` → `SHOW INDEX` 确认索引还在 - 常见失效清单:LIKE 左模糊 / OR 拼接 / 隐式转换 / 函数包装 / NOT IN / != / `< >` / IS NULL / ORDER BY 非索引列 / charset 不一致 - 强制手段:`ANALYZE TABLE` 刷新统计 → `FORCE INDEX` 硬推 → 关闭 `index_merge` 观察 → 拆分 SQL 到合适索引

完整讲解

第一部分:知识重点剖析

**核心考点定位**:掌握一套从"发现未走索引"到"定位失效原因"再到"给出修复方案"的完整排查链。

**知识点拆解**:
1. **确认索引确实存在**:`SHOW INDEX FROM t` 看索引列、`Cardinality` 基数是否正常、`Non_unique` 是否唯一。索引不存在或列写错是最基础的可能。
2. **确认 SQL 真的没走索引**:
- `EXPLAIN` 看 `key` 列是否为 `NULL`、`rows` 是否等于全表、`Extra` 是否有 `Using filesort` / `Using temporary`
- `EXPLAIN FORMAT=JSON` 看 `missing_covering_index` 建议、`cost_info.query_cost`
- `sys.schema_index_statistics`:`rows_selected / rows_examined` 长期低于 1% 说明索引扫描了大量无用行
- `performance_schema.table_io_waits_summary_by_index_usage`:索引长期 `count_star=0` 说明这个索引根本没人用
- MySQL 8.0:`EXPLAIN ANALYZE` 看真实执行代价
3. **常见失效场景清单**:
- **隐式类型转换**:字符串列 vs 数字条件(`WHERE varchar_col = 123`),MySQL 会强制把 varchar 转 number,索引失效
- **字段上做函数/运算**:`WHERE YEAR(create_time)=2024`、`WHERE UPPER(name)='A'`、`WHERE age+1=10`
- **字符集/排序规则不一致**:JOIN 时两表字段 charset 或 collation 不同
- **左模糊**:`WHERE name LIKE '%ab'`,前缀不确定无法用 B+ 树有序性
- **OR 拼接非索引列**:`WHERE a=1 OR non_index=2` 退化为全表
- **`NOT IN`、`!=`、`< >`、`IS NOT NULL`**:范围或反向判断,优化器倾向全表
- **`ORDER BY` / `GROUP BY` 非索引列**:引发 `filesort`,若排序后需要大量回表,优化器可能放弃索引
- **多值匹配**:`WHERE FIND_IN_SET('x', col)`、`col LIKE '%a%'` 全失效
- **`IN` 元素过多**:几百个值优化器倾向全表(受 `eq_range_index_dive_limit` 影响)
4. **修复方案**:
- 改写 SQL:函数包装 → 反向范围条件(`YEAR(t)=2024` → `t >= '2024-01-01' AND t < '2025-01-01'`)
- 加索引:新建覆盖索引 / 联合索引
- 强制手段:`ANALYZE TABLE` 刷新统计信息、`FORCE INDEX` 硬推、临时 `optimizer_switch` 关闭 index_merge
- 索引设计:把高频查询列放到联合索引前面,把选择性高的列放前面

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清"为什么某个 SQL 没走索引"的优化器决策逻辑(cost 模型)
- **广度**:能否覆盖 sys schema、performance_schema、INFORMATION_SCHEMA 三层视图
- **应用**:能否在真实线上慢查询中快速定位并给出修复方案

**延伸递进面试题**:
1. **为什么 `EXPLAIN` 显示的 `rows` 可能比实际扫描行数大或小很多?**
- 解答要点:`rows` 是基于统计信息的估算,`ANALYZE TABLE` 后会更准;8.0 用 `EXPLAIN ANALYZE` 看真实值。
2. **`IN (1,2,...,500)` 里的 500 个值为什么不走索引?**
- 解答要点:受 `eq_range_index_dive_limit`(默认 200)限制,超过阈值优化器用 `statistics` 估算而不是精确 `index dive`,估算偏大导致选全表。可以 `SET eq_range_index_dive_limit=1000`。
3. **`sys.statement_analysis` 里怎么定位长期不走的索引?**
- 解答要点:JOIN `schema_index_statistics` 看 `count_star=0` 且 `rows_examined>0` 的索引,是候选可删除索引;再结合 `EXPLAIN` 验证是否真的没人用。

第三部分:面试者参考回答(口语化)

面试官您好,索引失效排查我一般分三步走。

第一步是**确认现象**。跑一下 `EXPLAIN`,重点看三个字段:`key` 是不是 NULL,如果是就是没走索引;`rows` 是不是接近全表;`Extra` 里有没有 `Using filesort` 或 `Using temporary`。如果不确定,可以再跑 `EXPLAIN FORMAT=JSON`,里面会有 `missing_covering_index` 建议和 `cost_info` 的估算代价,MySQL 8.0 还有 `EXPLAIN ANALYZE` 能看真实执行耗时。

第二步是**定位原因**,按几个高频场景去排查。第一类是**隐式转换**,比如 varchar 列跟数字比较,`WHERE code=123` 而不是 `WHERE code='123'`,MySQL 会把 varchar 强制转 number,索引就废了。第二类是**字段上加了函数或运算**,比如 `WHERE YEAR(create_time)=2024`、`WHERE UPPER(name)='A'`、`WHERE age+1=10`,这些都会让索引失效,标准改法是反向写成范围条件 `create_time >= '2024-01-01' AND create_time < '2025-01-01'`。第三类是**查询模式问题**,`LIKE` 左模糊、`OR` 拼接非索引列、`NOT IN`、`!=`、`< >`、`IS NOT NULL`,这些都会让优化器倾向全表。第四类是**排序或分组问题**,`ORDER BY` 一个非索引列会触发 filesort,如果文件排序代价高,优化器会放弃索引走全表。第五类是**统计信息过期**,MySQL 是基于采样估算的,数据倾斜严重时会选错。

第三步是**修复**。SQL 能改就改,把函数包装改成范围条件。改不了的加索引,联合索引要按高频查询列的顺序放。如果怀疑是统计信息不准,先跑 `ANALYZE TABLE` 刷新一下。真要保底可以 `FORCE INDEX` 硬推,但要配合 `EXPLAIN ANALYZE` 验证真实代价,别引入更差的全索引扫描。

日常我也会盯着 `sys.schema_index_statistics` 和 `performance_schema.table_io_waits_summary_by_index_usage`,发现 `count_star=0` 或者 `rows_selected/rows_examined` 长期低于 1% 的索引,就是失效或者无效索引,该改该删。

https://chat.deepseek.com/share/jqfr120k19i2vcl6e3

#6 乐观锁与悲观锁如何实现? medium mysql

题目

乐观锁与悲观锁如何实现?

我的回答

MySQL 中乐观锁与悲观锁。乐观锁其实就是比较交换,先比较一下,比如设一个版本号,版本比较之前跟比较之后,如果是比较之前是一致的,然后就可以更新,如果版本号不一致就不更新。所谓乐观锁就是觉得它没什么问题,不用加锁,然后如果发生了变动才判定这个锁失效了,就不再执行。悲观锁的话,就是直接强加锁,for update,update 这种直接就是本身的 MySQL 里边它自己的锁,它本身的锁就是一个悲观锁的执行,并不是乐观锁。比如它的行锁、列锁、间隙锁,这些其实全都是悲观锁。如何实现呢?悲观锁它实现的基于场景其实是在索引上加锁,把当前的要处理的行上加一个锁,或者是对应的一个空间,一个锁的,在区间内加锁,区间锁,锁住这部分数据,在其他的执行时候,其他数据执行来过来执行的时候,如果是更新的写锁,其他来读死都不能读,然后就会导致对应的数据,让其他的 SQL 来执行的话,事务是不可以查到的。

点评

优点:- 乐观锁核心正确:版本号 + CAS 思想,冲突则不更新 - 悲观锁核心正确:`SELECT FOR UPDATE` / `UPDATE` / `DELETE` 隐式加锁,MySQL 原生锁属于悲观锁 - 提到行锁、间隙锁等具体类型
不足:- 未说明 MySQL 里乐观锁**没有原生实现**,需要应用层配合版本号列 - 未给出标准 SQL 写法:`UPDATE ... SET version=version+1 WHERE id=? AND version=?` - 未提冲突后重试策略(重试次数、退避) - 未提悲观锁加锁粒度:`SELECT FOR UPDATE NOWAIT` / `SKIP LOCKED`(MySQL 8.0+) - 未提共享读锁:`LOCK IN SHARE MODE`(8.0 起 `FOR SHARE`) - 未提锁与隔离级别的联动:RC 下无间隙锁,RR 下有 - 未提死锁检测(`innodb_deadlock_detect`)、`SHOW ENGINE INNODB STATUS` 查看锁信息 - 未提选型场景:低冲突高频读用乐观、高冲突用悲观
补充:- 乐观锁标准模式:`version` 列 + CAS 更新,影响行数 0 即冲突 - 悲观锁语法:`SELECT ... FOR UPDATE`(排他)、`SELECT ... FOR SHARE`(共享)、`LOCK IN SHARE MODE`(5.7 语法) - MySQL 8.0 新增:`FOR UPDATE NOWAIT` 立即失败、`FOR UPDATE SKIP LOCKED` 跳过加锁行(做队列消费超常用) - 死锁处理:`innodb_deadlock_detect=ON`(默认),通过 wait-for graph 检测,回滚 undo log 少的 - 选型:库存扣减、余额修改这类高冲突场景用悲观;点赞、计数这类低冲突场景用乐观

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解乐观锁与悲观锁的本质差异(冲突时机的假设不同),掌握 MySQL 中的具体实现方式与选型判断。

**知识点拆解**:
1. **本质差异**:
- **悲观锁**:假设"我一定会冲突",操作前先加锁,其他事务阻塞等待
- **乐观锁**:假设"大概率不会冲突",不加锁,更新时校验版本,冲突才失败重试
- 关键区别:**悲观锁串行化访问,乐观锁并发执行 + 事后校验**
2. **MySQL 中的乐观锁实现**:MySQL 本身没有乐观锁,需要在应用层做。标准做法是加 `version` 列:
```sql
UPDATE t SET col=?, version=version+1 WHERE id=? AND version=?;
```
影响行数 = 1 表示成功;影响行数 = 0 表示版本冲突,业务层选择重试或失败。这也是 **CAS(Compare-And-Swap)** 的数据库版本。
3. **MySQL 中的悲观锁实现**:
- `SELECT ... FOR UPDATE`:加排他锁(X 锁),其他事务读/写都要等
- `SELECT ... FOR SHARE`(8.0+)/ `LOCK IN SHARE MODE`(5.7):加共享锁(S 锁),其他事务读可以,写要等
- `UPDATE` / `DELETE`:隐式加 X 锁(不需要显式声明)
- `INSERT`:隐式加 S 锁(gap + insert intention lock)
4. **加锁粒度**:
- **行锁**:锁定单行,最常用
- **临键锁(Next-Key Lock)**:锁定一行 + 后面的间隙,RR 隔离级别下默认,防止幻读
- **间隙锁(Gap Lock)**:只锁间隙,不锁记录,RR 独有
- **表锁 / MDL**:DDL 会加 MDL,阻塞 DML
5. **MySQL 8.0 增强**:
- `FOR UPDATE NOWAIT`:拿不到锁立即返回错误,不阻塞
- `FOR UPDATE SKIP LOCKED`:跳过已加锁的行,用于"队列消费"模式
- `LOCKED_COLUMNS=NONE`:锁表而不是锁行
6. **死锁与监控**:
- `SHOW ENGINE INNODB STATUS` 看 `LATEST DETECTED DEADLOCK` 段
- `information_schema.innodb_trx` 查看当前事务
- `information_schema.innodb_lock_waits`(8.0 之前)/ `performance_schema.data_lock_waits`(8.0)看锁等待链
- `innodb_lock_wait_timeout` 控制等待超时(默认 50 秒)
- `innodb_deadlock_detect=ON`(默认)通过 wait-for graph 检测死锁,回滚 undo log 少的
7. **选型原则**:
- **乐观锁适用**:读多写少、冲突率低(点赞计数、库存扣减中低并发场景)、能容忍重试
- **悲观锁适用**:读少写多、冲突率高、必须强一致(余额、订单支付、账户转账)
- 悲观锁的代价:并发度低、有死锁风险、有超时问题

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清乐观锁/悲观锁的假设差异,以及 MySQL 里为什么乐观锁要放在应用层
- **广度**:能否关联到隔离级别、死锁检测、锁等待监控
- **应用**:能否针对具体业务场景给出选型建议

**延伸递进面试题**:
1. **`SELECT FOR UPDATE` 在 RC 和 RR 隔离级别下锁的粒度有什么区别?**
- 解答要点:RC 下只加当前读的行锁,无间隙锁;RR 下加 next-key lock,包含间隙锁,防止幻读。
2. **`FOR UPDATE SKIP LOCKED` 可以用来做什么?**
- 解答要点:任务队列场景。多个 worker 抢任务时,跳过已被其他 worker 加锁的行,避免互相阻塞。适合 Redis 之外的 DB 队列实现。
3. **乐观锁的 `version` 列,遇到重试风暴怎么办?**
- 解答要点:加指数退避 + 最大重试次数;核心业务可切换到悲观锁或分布式锁;也可用 CAS 型 `UPDATE ... WHERE old_val=?` 代替 version。

第三部分:面试者参考回答(口语化)

面试官您好,乐观锁和悲观锁本质上是两种对"冲突概率"的假设。悲观锁假设"我一定会冲突",操作前先加锁,别人只能等;乐观锁假设"大概率不会冲突",先不加锁直接干活,提交前再校验,冲突才失败重试。

**悲观锁在 MySQL 里是原生支持的**。核心语法是 `SELECT ... FOR UPDATE`,它会给读到的行加排他锁,其他事务读、写都得等。8.0 之后还有 `SELECT ... FOR SHARE`,加的是共享锁,其他事务读可以并行,只有写要等。`UPDATE` 和 `DELETE` 本身就会隐式加 X 锁,不需要显式声明。加锁粒度主要有行锁、临键锁(next-key lock)、间隙锁,RR 隔离级别下默认用临键锁防止幻读。

8.0 还有两个增强语法我觉得很实用:`FOR UPDATE NOWAIT`,拿不到锁立即报错不阻塞;`FOR UPDATE SKIP LOCKED`,跳过已经被加锁的行,特别适合任务队列场景——多个 worker 抢任务,跳过的行让下一个 worker 处理,互不干扰。

**乐观锁 MySQL 没有原生实现,要在应用层做**。标准做法是表里加一个 `version` 列,更新的时候带上版本条件:`UPDATE t SET col=?, version=version+1 WHERE id=? AND version=?`。这条 SQL 的影响行数如果是 1,说明成功;如果是 0,说明别人在我读和写之间改过了,版本对不上,业务层决定是重试还是失败。这本质就是 CAS,Compare-And-Swap 的数据库版本。

**选型上**,悲观锁适合高冲突、强一致的场景,比如余额扣减、订单支付;乐观锁适合低冲突、能容忍重试的场景,比如点赞计数、普通库存扣减。悲观锁的代价是并发度低、有死锁风险、有超时问题,MySQL 默认 `innodb_lock_wait_timeout=50s`,超过就报错。死锁的话 `innodb_deadlock_detect=ON`(默认开启),InnoDB 会通过 wait-for graph 检测到死锁,回滚 undo log 少的那个事务。要排查线上死锁看 `SHOW ENGINE INNODB STATUS` 的 LATEST DETECTED DEADLOCK 段,或者查 `performance_schema.data_lock_waits` 看锁等待链。

https://chat.deepseek.com/share/1f4jfjd7lcbtnwc0hu

#7 MySQL为什么会选错索引,如何解决? medium mysql

题目

MySQL为什么会选错索引,如何解决?

我的回答

MySQL 走索引,可能是预期不符,它计算的执行计划里边,它看到的成本可能与我们预估的不一样,或者是存在计算的时候,它统计的数据和实际的数据存在误差。这时候就会导致它计算的成本走它其他的索引,而不选择我们预期中的索引。这时候我们可以其实可以做强走索引,或者是重新生成索引,把数据索引数据刷新一下。不是重新生成索引,是刷新索引。这时候的话可以让 MySQL 去重新计算这个索引的成本,然后可以看一下是否走了我们预期索引。然后就是实在不行就强加索引,走强制索引,我们指定强制我们那个索引,但加强制索引有风险。因为会导致一些死硬的编程,这因为加强制索引会写到 SQL 里边。之前碰见过类似的情况,就是加强制索引后,对应的这个字段的索引后续觉得没有用了被删除了,但是业务里边有加强索引的地方,直接导致对应业务报错生产事故。

点评

优点:- 抓到本质:优化器基于成本估算决策,采样统计与实际分布有偏差 - 提到 `ANALYZE TABLE` 刷新统计信息 - 有真实生产事故案例(`FORCE INDEX` 硬编码导致后续索引被删报错),有实战感
不足:- 未具体讲成本模型:`cost = rows × cpu_cost + pages × io_cost` - 未提采样基数:`innodb_stats_persistent_sample_pages`(默认 20 页),倾斜数据下必翻车 - 未提 MySQL 8.0 的**直方图**:`ANALYZE TABLE t UPDATE HISTOGRAM ON col`,专门解决长尾分布 - 未提**优化器开关**:`optimizer_switch`、`eq_range_index_dive_limit`、`prefer_ordering_index`、`index_merge` - 未提 `EXPLAIN FORMAT=JSON` 里 `cost_info` / `attached_condition` 对比 - 未提 `IGNORE INDEX` 与 `FORCE INDEX` 的区别 - 未提索引治理:`performance_schema.table_io_waits_summary_by_index_usage.count_star=0` 找无索引
补充:- 选错索引的三大根因:统计信息过期 / 数据倾斜(采样失真)/ `IN (...)` 元素过多越过 `eq_range_index_dive_limit` - 修复优先级:改写 SQL > 补索引 > 刷新统计 > FORCE INDEX - FORCE INDEX 上线前必须 `EXPLAIN ANALYZE` 验证真实代价 - 8.0 更推荐 hints:`/*+ IGNORE_INDEX(t idx) */` 或 `/*+ FORCE_INDEX(t idx) */`

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解 MySQL 优化器基于成本模型的决策机制,掌握选错索引的诊断与修复手段。

**知识点拆解**:
1. **优化器成本模型**:MySQL 使用基于成本的优化器(CBO),核心公式 `cost = cpu_cost × rows + io_cost × pages_read`。它会为每种可能计划算 cost,选最小的。cost 是基于**统计信息估算**出来的,不是真实扫描值,这就是选错索引的根源。
2. **统计信息从哪来**:InnoDB 持久化统计(`innodb_stats_persistent=ON`),用 `innodb_stats_persistent_sample_pages`(默认 20 页)随机采样估算基数。**采样只有 20 页**,数据倾斜、热点集中在少数页时估算必失准。
3. **选错索引的典型场景**:
- **统计信息过期**:数据大量增删后没 `ANALYZE`,优化器用旧基数算
- **数据倾斜**:某个值占比 90%,采样页没扫到,优化器以为"很分散"
- **`IN` 元素过多**:超过 `eq_range_index_dive_limit`(默认 200),从精确 `index dive` 切换成基于统计估算
- **多索引可选**:`(a)`、`(a,b)`、`(a,c)` 都存在,优化器挑了错误的
- **`ORDER BY` 排序列**:优化器偏好 `prefer_ordering_index=on`,即使成本更高也倾向避免 filesort
- **`index_merge` 陷阱**:优化器认为 index merge 更快但实际更慢
4. **诊断手段**:
- `EXPLAIN` 看 `key`、`rows`、`Extra`、`key_len`
- `EXPLAIN FORMAT=JSON`:看 `cost_info.query_cost`、`attached_condition`、`missing_covering_index`
- `EXPLAIN ANALYZE`(8.0):看**真实执行耗时**与估算的差距
- `SHOW INDEX FROM t`:看 `Cardinality` 是否合理
- `sys.statement_analysis`:找慢查询关联的 index 使用
5. **修复手段(按优先级)**:
1. **改写 SQL**:把无法用索引的写法改掉(去掉函数包装、拆 `OR` 为 `UNION`)
2. **补索引/调索引**:加更贴合查询的索引
3. **刷新统计**:`ANALYZE TABLE` 或 8.0 建**直方图** `ANALYZE TABLE t UPDATE HISTOGRAM ON col`
4. **优化器开关**:调 `optimizer_switch`、`eq_range_index_dive_limit`
5. **FORCE INDEX / IGNORE INDEX**:硬指定,最后兜底
6. **8.0 Optimizer Hints**:`/*+ FORCE_INDEX(t idx) */`,写在 SQL 里同样有硬编码风险
6. **工程治理**:
- 定期扫描 `performance_schema.table_io_waits_summary_by_index_usage`,`count_star=0` 的索引是候选删除
- 建索引、删索引要有 CR 流程,避免"索引删了但 SQL 还在 FORCE INDEX"
- 生产环境变更前跑 `EXPLAIN ANALYZE` 看真实代价

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清成本模型的估算逻辑,采样为什么会导致失真
- **广度**:能否覆盖统计信息、优化器开关、直方图、Hints 全链条
- **应用**:能否在真实线上事故中给出一套排查与兜底方案

**延伸递进面试题**:
1. **`eq_range_index_dive_limit` 是什么?为什么超过它反而选错?**
- 解答要点:控制 `IN` 或范围条件里 index dive 的最大次数。超过后优化器退回用 `stats.records` 均分估算,倾斜数据下偏差巨大。
2. **`prefer_ordering_index` 是干什么的?为什么会导致"更贵的索引被选中"?**
- 解答要点:默认 `on`,优化器偏好能避免 filesort 的索引。即使该索引要扫描更多行,只要省了排序就选它。
3. **MySQL 8.0 直方图是什么?什么时候该建?**
- 解答要点:`ANALYZE TABLE t UPDATE HISTOGRAM ON col` 会把列的值分布采样存下来,帮助优化器更准地估算基数。适合长尾分布(如 status 字段 90% 都是 1)场景。

第三部分:面试者参考回答(口语化)

面试官您好,MySQL 选错索引本质上是基于成本的优化器(CBO)算错了成本。它做决策的核心是 `cost = rows × cpu_cost + pages × io_cost`,`rows` 和 `pages` 都来自统计信息,而统计信息是 InnoDB 通过 `innodb_stats_persistent_sample_pages` 采样估算出来的,默认只采样 20 页。所以只要数据有倾斜、采样页没覆盖到热点分布,优化器就会算错行数,选到错误的索引。

典型场景有几个:一是**统计信息过期**,数据大量增删后没跑 `ANALYZE`,优化器用旧数据算;二是**数据倾斜**,某个值占比 90% 但采样没扫到;三是 `IN` 元素超过 `eq_range_index_dive_limit`(默认 200),优化器从精确 index dive 退回均分估算;四是**排序偏好**,`prefer_ordering_index=on` 让优化器倾向选能避免 filesort 的索引,即使要扫更多行。

排查上我会先跑 `EXPLAIN` 看 `key`、`rows`、`Extra`,再用 `EXPLAIN FORMAT=JSON` 看 `cost_info` 和 `attached_condition`,MySQL 8.0 最好用 `EXPLAIN ANALYZE` 直接看真实耗时和估算的差异,差距越大说明统计信息越不准。

修复我按优先级来:先**改 SQL**,把走不到索引的写法改掉(比如函数包装改范围条件,`OR` 拆 `UNION`);再考虑**补索引**或者调整联合索引顺序;然后跑 `ANALYZE TABLE` 刷新统计,如果是长尾分布的场景,MySQL 8.0 还可以建**直方图** `ANALYZE TABLE t UPDATE HISTOGRAM ON col`;实在不行再动 `optimizer_switch` 或者硬指定。

`FORCE INDEX` 我一般放最后手段。它写死在 SQL 里有两个风险:一是数据变化后强走可能变成全索引扫描反而更慢;二是我之前踩过坑——代码里 `FORCE INDEX` 硬编码,后来团队把这个索引当冗余删了,业务直接生产报错。所以我的原则是 `FORCE INDEX` 上线前必须 `EXPLAIN ANALYZE` 验证真实代价,而且要有索引治理流程,通过 `performance_schema.table_io_waits_summary_by_index_usage` 定期扫 `count_star=0` 的索引,删之前先 grep 代码库确认没人引用。

https://chat.deepseek.com/share/9fe4n94b1weytntp9b

#8 介绍下MySQL 5.7中的组提交? medium mysql

题目

介绍下MySQL 5.7中的组提交?

我的回答

主提交方面我理解不深,但是按我的理解,主提交基本上就类似于批量的提升效率的功能。按照正常的批量操作策略来说,应该是预提交,然后验证或者执行,然后最后是提交 commit,中间出问题可能会涉及回滚或者是部分提交类似的情况。主提交这种东西基本上是批量操作,就是提升效率,提升,降低刷盘,降低 IO,然后提升单次效率。

点评

优点:- 大方向抓住:**组提交 = 批量刷盘降 fsync 次数**
不足:- **术语错误**:全程说成"主提交",正确叫法是**组提交(Group Commit)**——这可能是口误或混淆 - **过于笼统**:只提到"批量",没讲具体机制,无法体现深度 - 未区分**两种组提交**:binlog 组提交(5.7 引入)vs redo log 组提交(一直有) - 未提三阶段流程:**Flush → Sync → Commit** - 未提关键参数:`sync_binlog`、`innodb_flush_log_at_trx_commit` - 未提**双 1 配置**下每组事务的 fsync 次数 - 未提 5.7 引入的 **group leader 机制**:多线程 binlog 写入优化
补充:- binlog 组提交:一个事务的 binlog 写完不立刻 fsync,等下一个事务加入,多个事务合并 fsync 一次 - redo log 组提交:InnoDB 的 WAL 机制,多个事务的 redo 一起 fsync 到磁盘 - **binlog + redo 联合提交(2PC 增强版)**:5.7 起 binlog group commit 与 redo log group commit 在 commit 阶段联动,减少总 fsync 次数 - 关键参数:`sync_binlog=1`(每次都刷 binlog)、`innodb_flush_log_at_trx_commit=1`(每次都刷 redo),这是"双 1 配置",性能 vs 一致性权衡 - 优化收益:`sync_binlog=1000` 或 `=0` 时性能提升明显但可能丢事务

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解 MySQL 5.7 binlog 组提交的三阶段模型与 fsync 合并优化,掌握其在双写(binlog + redo)场景下的收益。

**知识点拆解**:
1. **背景:为什么需要组提交**
- 事务提交时必须保证 binlog 和 redo log 都刷盘(**双 1 配置**),否则可能丢失或数据不一致
- 单个事务提交要 fsync 两次:一次 binlog,一次 redo log
- fsync 是磁盘 IO 的瓶颈,一次 fsync 可能几毫秒
- 高并发下每秒数千事务,fsync 次数成为吞吐瓶颈
2. **什么是组提交**
- **binlog 组提交**(MySQL 5.7 引入,5.6 前已有简化版):多个事务的 binlog 写完后合并 fsync
- **redo log 组提交**(InnoDB 一直有):多个事务的 redo log 写完后合并 fsync
- 本质:把 N 个事务的 2N 次 fsync 合并成 2 次
3. **binlog 组提交的三阶段模型**
- **Flush 阶段**:每个事务把 binlog 从 page cache 刷到 OS cache(顺序写,快),此时事务独立进行,无排队
- **Sync 阶段**:一个事务(Group Leader)等所有事务 flush 完后,代表组执行 fsync,把 binlog 刷到磁盘;组内其他事务等待
- **Commit 阶段**:fsync 完成后,所有事务一起更新 binlog 状态、通知主线程 commit,进入 redo log 提交
4. **Group Leader 机制**(5.7 新特性)
- 只有一个事务是 Group Leader,其他是 Group Follower
- Group Leader 负责在 Sync 阶段做 fsync,Group Follower 等待
- 组越大,Leader 的 fsync 覆盖的事务越多,每个事务平摊成本越低
- 5.7 引入**组提交并发优化**:允许多个 Group Leader 交替工作,减少组间阻塞
5. **redo log 组提交**
- InnoDB 的 WAL 机制,事务修改先写 redo log,不立刻刷脏页
- 多个事务的 redo 可以合并 fsync
- `innodb_flush_log_at_trx_commit`:1=每次 commit fsync,0=每秒 fsync(可能丢 1 秒数据),2=写 OS cache 不 fsync(MySQL 崩溃不丢,系统崩溃可能丢)
6. **5.7 的关键优化:联合提交**
- binlog 组提交的 Commit 阶段触发 InnoDB redo log 组提交的 fsync
- 相当于 binlog 和 redo 的组提交"绑在一起"完成
- 总 fsync 次数从"2N"降到"2"(每组),性能提升显著
7. **性能影响**
- `sync_binlog=1`(默认,最安全)+ 组提交:每组 fsync 一次 binlog
- `sync_binlog=1000`:每 1000 个事务 fsync 一次 binlog,崩溃最多丢 1000 个事务
- `sync_binlog=0`:由 OS 决定 fsync 时机,最快但可能丢大量数据
- 生产环境通常:双 1 配置保安全 + 依赖组提交提性能

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清 Flush / Sync / Commit 三阶段模型,Group Leader 机制
- **广度**:能否关联到 2PC、WAL、redo log 组提交
- **应用**:能否根据业务容忍度选择 `sync_binlog` 与 `innodb_flush_log_at_trx_commit`

**延伸递进面试题**:
1. **binlog 组提交的 Commit 阶段和 InnoDB 的 2PC 有什么关系?**
- 解答要点:binlog 组提交的 Commit 阶段触发 InnoDB 的 redo log prepare→commit 两阶段,两者组提交在此联动。
2. **`sync_binlog=1` 和 `innodb_flush_log_at_trx_commit=1` 都能保证零丢失吗?**
- 解答要点:只能保证 MySQL 层不丢(binlog 有、redo 有),但操作系统崩溃时 OS cache 未落盘仍会丢。要真正持久需要 `O_DIRECT` + `fdatasync`。
3. **MySQL 8.0 对组提交有哪些优化?**
- 解答要点:8.0 引入了 binlog 多线程写入(binlog 组提交优化)、GTID 压缩、`binlog_transaction_compression` 等。

第三部分:面试者参考回答(口语化)

面试官您好,MySQL 5.7 的组提交,主要是解决高并发下事务提交时 binlog 和 redo log 频繁 fsync 的性能瓶颈。要理解组提交,先要理解一个事务提交要经过什么。

在双 1 配置下(`sync_binlog=1` + `innodb_flush_log_at_trx_commit=1`),单个事务提交最少需要 2 次 fsync:一次刷 binlog,一次刷 redo log。fsync 是磁盘 IO 的瓶颈,一次可能几毫秒,高并发下每秒几千个事务就要几千次 fsync,直接把磁盘打爆。组提交就是把多个事务"打包"起来,一次 fsync 覆盖多个事务。

**binlog 组提交分三个阶段**:

**Flush 阶段**:每个事务把 binlog 从 MySQL 自己的 page cache 刷到 OS cache。这一步是顺序写,很快,每个事务独立进行,不需要排队。

**Sync 阶段**:关键的一步。事务进入这个阶段后要排队,由一个事务当 Group Leader 代表整组事务做 fsync,把 binlog 从 OS cache 刷到磁盘。其他事务作为 Group Follower 等待。这里组越大,fsync 成本被平摊得越薄。

**Commit 阶段**:Leader fsync 完成后,通知所有 Follower 一起更新 binlog 状态。同时这一步会触发 InnoDB 的 redo log 提交——**binlog 的 Commit 阶段和 InnoDB 的 redo 组提交在这里联动**,相当于 2PC 增强版:binlog 和 redo 一起提交,一次 fsync 完成两件事。

MySQL 5.7 的 Group Leader 机制引入了**多个 Group Leader 交替工作**,允许组之间的并行度更高,减少了组与组之间的阻塞。

**为什么要这么麻烦**:因为 fsync 慢,但事务提交不能丢 binlog 或 redo,只能靠组提交把 fsync 次数从"每事务 2 次"降到"每组 2 次",性能可以从几百 TPS 提升到几千甚至上万 TPS。

**参数选择**:`sync_binlog=1` 是最安全的配置,崩溃不丢;`sync_binlog=1000` 是每 1000 个事务 fsync 一次,能提升性能但可能丢最多 1000 个事务;`sync_binlog=0` 交给 OS 决定,最快但可能丢大量数据。生产上通常是双 1 配置保安全,靠组提交拿性能,牺牲一点 CPU 换取数据可靠。

https://chat.deepseek.com/share/dsrk0ch0l2279ncpir

#9 设计索引的时候有哪些原则(考虑哪些因素)? high mysql

题目

设计索引的时候有哪些原则(考虑哪些因素)?

我的回答

设计索引要考虑的核心机制,首先是业务第一。基于什么样的业务去设计什么样的索引,不能创建一个没有意义的索引。你创建了一个特别好的索引,但这个索引没有意义,创建的是没有用的。 首先创建索引要在区分度比较高的字段上进行创建。如果一个字段区分度特别低,各种数据一样占一半,或者占的比例都很多,没有什么区分度,那么这个索引的意义也不大。 要解决的核心目标是什么?是为了快速的找到我们目标的数据。前置说的区分度高的,就是为了这个;然后整个索引的过程会涉及整个搜索的过程会涉及几个方面。首先是通过索引找到数据,然后如果你要显示的数据在索引中不存在,就会涉及一个回表。这里边要考虑的就是你的索引是否要进行覆盖,覆盖索引的所有字段。如果能覆盖,就会减少一次搜索。 另外就是查询的时候会涉及排序,排序的字段是否要包含在索引中?尽可能的其实要把排序字段在索引中体现出来,这样可以更好的利用索引提前进行排序,而不是搜索出来之后再进行外部的文件排序,那样会浪费更多的时间和资源。 还有就是对于数据量的考量,数据量级具体在什么量级,使用什么样的索引,不同量级的话可能要考虑提前做分库分表,这里不仅仅是索引方面的问题,还有对数据量评估,以什么维度去进行数据库的拆分。 这就是我考虑的创建索引时的主要原则。

点评

优点:- **业务第一**:抓到了最本质的原则——有查询模式才建索引 - **区分度**:明确说低区分度字段建索引意义不大 - **覆盖索引减少回表**:能识别回表成本,理解覆盖索引的价值 - **排序字段放索引避免 filesort**:抓到了 `ORDER BY` 优化要点 - **数据量级考量**:意识到数据量大会走向分库分表
不足:- **未讲联合索引列顺序**:等值列放前、范围列放后、排序列放最后——这是索引设计最关键的细节 - **未讲写放大成本**:每个索引都要维护 B+ 树,写多读少场景要克制 - **未讲唯一索引**:既能防重又能加速,业务唯一字段必须建 - **未讲主键选择**:自增 bigint vs UUID,UUID 会导致 B+ 树页分裂和随机 IO - **未讲前缀索引**:TEXT/BLOB 字段用 `INDEX(col(20))` 而非整列 - **未讲索引长度限制**:InnoDB 默认 3072 字节(8.0 `COMPRESSED/DYNAMIC`) - **未讲 JOIN 场景**:关联字段必须建索引,否则右表全表扫描 - **未讲 NOT NULL**:索引列允许 NULL 会增加 NULL bitmap 开销 - **未讲冗余治理**:`(a,b,c)` 已存在时 `(a,b)`、`(a)` 冗余;`schema_unused_indexes`、`INVISIBLE INDEX` 灰度下线 - **未讲 8.0 新特性**:`FUNCTIONAL INDEX`(表达式索引)、`DESCENDING INDEX`(降序索引) - 与首次(2026-09-17)相比,评分从 3/5 提升到 4/5,说明对"覆盖索引、排序、业务驱动"这三个点已经掌握到位
补充:- 索引设计 5 问:谁查、查多少、多频繁、要不要排序、写多还是读多 - 联合索引列顺序速查:`WHERE a=? AND b>? ORDER BY c` → `(a, b, c)` - 治理工具:`sys.schema_unused_indexes`、`performance_schema.table_io_waits_summary_by_index_usage.count_star=0` - 灰度下线:MySQL 8.0 `ALTER TABLE t ALTER INDEX idx INVISIBLE` 观察再删 > ℹ️ 完整讲解见上方首次记录(2026-09-17)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:以业务查询模式为锚点,系统掌握索引设计的全维度决策框架,覆盖数据、访问、性能、治理四条主线。

**知识点拆解**:

1. **业务维度:先看查询,再定索引**
- **查询频率**:一个索引每月只被用几次,收益远不如维护成本
- **查询模式**:等值 / 范围 / 排序 / 分组 / JOIN,每个模式对索引要求不同
- **数据量**:小表(< 1 万行)优化器倾向全表;大表必须靠索引
- **读写比**:读多写少 → 多建索引;写多读少 → 少建但建好

2. **基数(Cardinality)与选择率**
- 基数 = `distinct_count / total_rows`
- 基数接近 1(如 ID、订单号)→ 单列索引最有效
- 基数很低(如 gender、status 只有 2-3 值)→ 单列索引几乎无用,考虑放联合索引前缀或 Skip Scan
- 中基数(如 city、user_id)→ 单列索引常见
- `SHOW INDEX FROM t` 里的 `Cardinality` 是采样估算,用 `SELECT COUNT(DISTINCT col), COUNT(*) FROM t` 拿真实值

3. **联合索引的列顺序原则(关键)**
- **等值条件列放前**:`WHERE a=? AND b>?` → 索引顺序 `(a,b)` 才对
- **范围条件列放后**:范围列后面的列无法用索引定位(跨范围无序)
- **排序列放最后**:`ORDER BY` 的列如果放在联合索引最后,且前面都是等值,可以省 filesort
- **高基数列尽量靠前**:能更快收敛匹配行
- **避免低基数列开头**:如 `(status, user_id)`,status 基数 3 会导致扫描大量重复行

4. **覆盖索引优先**
- `SELECT` 的列全部命中某个索引 → 走覆盖索引,`Extra=Using index`
- 覆盖索引能省掉回表,性能提升数倍
- 权衡:索引宽度 vs 覆盖收益。避免为了覆盖把所有列都塞进一个索引,那样索引本身会膨胀

5. **回表数量控制**
- 回表次数 = 索引扫描到的行数,不是最终 LIMIT 数量
- `LIMIT 10` 的查询可能回表几百万次
- 优化:延迟关联、覆盖索引、ICP

6. **索引键长度限制**
- InnoDB 默认:`innodb_large_prefix=ON` 时 3072 字节(`COMPRESSED`/`DYNAMIC` 行格式)
- 旧版本 767 字节(`COMPACT`/`REDUNDANT` 行格式)
- TEXT/BLOB 不能整列建索引,必须用**前缀索引** `INDEX(name(20))`
- 前缀长度选择:`SELECT COUNT(DISTINCT LEFT(name, N)) / COUNT(DISTINCT name) FROM t`,达到 0.99+ 停止

7. **索引类型选择**
- B+ 树索引(默认):等值、范围、排序都能用
- 全文索引:中文模糊搜索场景,需要 `ngram` 分词器
- 空间索引:GIS 场景
- HASH 索引:仅 Memory 引擎支持;InnoDB 的 adaptive hash index 是自动的,无法手动控制
- MySQL 8.0 新增:`FUNCTIONAL INDEX`(表达式索引),`DESCENDING INDEX`(降序索引)

8. **冗余索引治理**
- **前缀冗余**:`(a,b,c)` 已存在时 `(a,b)`、`(a)` 是冗余
- **完全冗余**:`(a)` 和 `(a)` 重复
- **反向冗余**:`(a,b)` 和 `(b,a)` 都能用,但用途不同,通常保留一个
- 治理手段:
- `sys.schema_unused_indexes` 找 `count_star=0` 的索引
- `performance_schema.table_io_waits_summary_by_index_usage` 精确统计
- MySQL 8.0 `ALTER TABLE t ALTER INDEX idx INVISIBLE` 灰度下线,观察 1-2 周再 DELETE

9. **特殊场景**
- **JOIN 场景**:外键关联字段必须建索引,否则 join 时右表全表扫描
- **分库分表**:索引设计要考虑 shard key 是否在索引里,跨分片查询尤其要谨慎
- **唯一约束**:业务上唯一的字段必须建唯一索引,同时能防止重复插入,还能加速等值查询
- **主键选择**:优先自增 bigint,避免 UUID(无序、体积大、页分裂)
- **NULL 值**:索引列允许 NULL 会增加 NULL bitmap 开销,优先 NOT NULL + 默认值

10. **工程化治理**
- 索引变更走 CR:谁能加、谁能删要有流程
- `FORCE INDEX` 硬编码是雷区:索引被删业务立即崩
- 定期审计:每月扫一次 `schema_unused_indexes` 和慢查询
- 生产变更前必跑 `EXPLAIN ANALYZE`(8.0)看真实代价

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清联合索引列顺序、覆盖索引、基数选择背后的原理
- **广度**:能否覆盖索引类型、长度限制、8.0 新特性、分库分表
- **应用**:能否针对真实业务给出索引设计 + 治理的完整方案

**延伸递进面试题**:
1. **`(status, user_id, create_time)` 是常见设计,如果查询是 `WHERE status=1 ORDER BY create_time DESC`,这个索引能用吗?**
- 解答要点:能。status 等值命中前缀,`user_id` 未知但因为是联合索引的中间列——不对,中间列未知则 create_time 无法走排序。所以要改成 `(status, create_time)` 或直接建 `(status, create_time DESC)`(8.0 支持)。
2. **TEXT 字段做前缀索引怎么选长度?**
- 解答要点:跑 `SELECT COUNT(DISTINCT LEFT(col, N)), COUNT(DISTINCT col) FROM t` 让比值达到 0.99 或以上,同时兼顾空间。经验值:邮箱 20、手机号 11、用户名 10-15。
3. **MySQL 8.0 的 `INVISIBLE INDEX` 相比直接删除有什么好处?**
- 解答要点:可以先设置为不可见观察 1-2 周,确认没有 SQL 走这个索引后,再执行真正的 `DROP INDEX`。避免"删了才发现在用"的事故。DDL 也是在线的。

第三部分:面试者参考回答(口语化)

面试官您好,索引设计我按四条主线来讲:业务、数据、性能、治理。

**第一条,业务维度**。索引不是越多越好,一定要业务驱动——有查询模式才建索引。我会先摸清几个问题:谁在查、查多频繁、单条查询返回多少行、要不要排序、表是读多还是写多。小表几千行不用建,优化器自己全表扫;大表必须靠索引。写多读少的表要克制,每个索引都是 INSERT/UPDATE/DELETE 的额外维护成本。

**第二条,数据维度,重点是基数和联合索引列顺序**。基数就是 `distinct_count / total_rows`,越接近 1 越适合建单列索引,比如 ID、订单号;基数很低(如 status 只有 2-3 值)单独建索引基本没用,但可以放联合索引前面做前缀。联合索引的列顺序有讲究:等值条件列放前面、范围条件列放后面、`ORDER BY` 列放最后——这样能省 filesort。反过来如果排序列前面还有个未知列,排序就废了。

**第三条,性能维度,重点是覆盖索引和回表控制**。如果能把 SELECT 列全部塞进一个索引里,就走覆盖索引,EXPLAIN 显示 `Using index`,性能能提升好几倍。但索引宽度也有代价,不能什么都往里塞。回表次数取决于索引扫描行数而不是 LIMIT,所以要用延迟关联或 ICP 减少。索引键长度也有硬限制,InnoDB 默认 3072 字节,TEXT 字段必须做前缀索引,用 `SELECT COUNT(DISTINCT LEFT(col, N)) / COUNT(DISTINCT col)` 找最优长度。

**第四条,索引治理**。这块特别重要,很多线上事故出在这里。三个点:一是冗余治理,`(a,b,c)` 存在时 `(a,b)`、`(a)` 就是冗余,应该删掉;二是无索引治理,用 `sys.schema_unused_indexes` 或者 `performance_schema.table_io_waits_summary_by_index_usage.count_star=0` 找长期没人用的索引;三是灰度下线,MySQL 8.0 有 `ALTER TABLE t ALTER INDEX idx INVISIBLE`,可以先设为不可见观察一两周,确认没有 SQL 依赖后再真删。

特殊场景还有几个:JOIN 关联字段必须建索引;分库分表要考虑 shard key 是否在索引里;唯一约束优先用唯一索引既能防重又能加速;主键选自增 bigint 避免 UUID 打散 B+ 树导致页分裂和随机 IO;索引列最好 NOT NULL,避免 NULL bitmap 的额外开销。

最后工程化流程:任何索引变更走 CR,生产变更前跑 `EXPLAIN ANALYZE` 看真实代价,每月审计一次 `schema_unused_indexes`。特别警惕 `FORCE INDEX` 硬编码——团队有人删了这个索引而代码里还强推,业务直接生产报错,我之前踩过这个坑。

---

#10 什么是死锁?如何避免和解决 low mysql

题目

什么是死锁?如何避免和解决

我的回答

死锁就是一个彻底解不开的锁,表示这个动作被卡顿停止,没有人工介入是无法解除的。 构成死锁核心有这么几个原因。首先是资源没有一次性申请,不同的线程获取了不同的资源,又相互造成循环等待。 最后我申请资源,你也申请资源,大家申请的,你申请的是我的,我申请的是你的,我不能抢你的,你也不能抢我的,这里边就涉及一个不能抢占其他人资源,也是导致死锁的一个核心原因。 总结一下死锁构成的原因有,没有一次性申请所有资源,相互等待了资源,资源不能强制争抢。这 3 个任意破坏一个都可以解除这个死锁。

点评

优点:- 覆盖了部分死锁必要条件:占有且等待(没有一次性申请)、循环等待、不可剥夺 - 提到"任意破坏一个都可以解除",符合破坏条件防死锁的思路
不足:- **完全脱离数据库语境**:全程是操作系统通用理论,未落到 InnoDB 死锁的具体表现(`ERROR 1213`、wait-for graph、`innodb_deadlock_detect`、`innodb_lock_wait_timeout`) - **理论也讲得不够完整**:Coffman 四条件是互斥、占有且等待、不可剥夺、循环等待,本次遗漏了"互斥",且"任意破坏一个"实际只对"循环等待"和"占有且等待"这两条真正有工程意义——不可剥夺在 InnoDB 里几乎不能破坏 - **没讲 InnoDB 的死锁检测机制**:默认 `innodb_deadlock_detect=ON` 自动检测并回滚 undo log 少的事务,这是"自动解除"的关键 - **没讲监控**:`SHOW ENGINE INNODB STATUS` 的 LATEST DETECTED DEADLOCK 段、`performance_schema.data_lock_waits`、`innodb_trx` - **没讲业务侧解决方案**:固定加锁顺序、缩小事务粒度、乐观锁、NOWAIT、加索引防锁放大 - **没讲典型死锁场景**:间隙锁交叉、多行更新顺序不同、`INSERT ... ON DUPLICATE KEY UPDATE`
补充:- InnoDB 死锁本质:多事务因间隙锁 / 行锁 / Next-Key Lock 交叉形成 wait-for graph 环,检测器(默认开启)主动选一个牺牲者回滚 - 排查三件套:`SHOW ENGINE INNODB STATUS` 看最近死锁、`data_lock_waits` 看等待链、`innodb_trx` 找长事务 - 业务防死锁第一原则:**固定加锁顺序**(按主键升序) - 关键参数:`innodb_deadlock_detect`(检测开关)、`innodb_lock_wait_timeout`(默认 50 秒) > ℹ️ 完整讲解见上方首次记录(2026-09-17)

完整讲解

第一部分:知识重点剖析

**核心考点定位**:既要有操作系统层死锁的理论理解,更要掌握 InnoDB 死锁的检测、监控与业务侧规避手段。

**知识点拆解**:

1. **死锁理论(四必要条件)**:
- **互斥**:一次只能被一个事务持有
- **占有且等待**:持有 A 的同时等 B
- **不可剥夺**:事务持有的锁不能被动回收,必须主动释放
- **循环等待**:A 等 B、B 等 C、C 等 A 形成环
- 破坏任一条件即可防止死锁

2. **InnoDB 的死锁机制**:
- InnoDB 是**自动检测**型:默认 `innodb_deadlock_detect=ON`,事务在等锁时会持续构建 wait-for graph,一旦形成环就立刻识别出死锁
- **牺牲者选择**:回滚 undo log 少(改动小)的事务,让它抛出 `ERROR 1213`,另一事务继续执行
- 死锁检测成本:O(N²) 检查当前所有等锁事务,事务数很多时开销大
- 超时兜底:`innodb_lock_wait_timeout`(默认 50 秒),超过直接抛 `ERROR 1205: Lock wait timeout exceeded`,事务回滚

3. **InnoDB 常见死锁场景**:
- **间隙锁交叉**:两个事务在 RR 隔离级别下分别用 `INSERT` 或 `SELECT FOR UPDATE` 请求不同的间隙锁,最终循环等待
- **不同顺序更新多行**:事务 A 先锁 id=1 再锁 id=2,事务 B 先锁 id=2 再锁 id=1
- **`INSERT ... ON DUPLICATE KEY UPDATE`**:并发插入相同主键,一个 INSERT 一个 UPDATE 触发死锁
- **索引缺失导致的锁升级**:本该走索引却全表扫描,锁大量行放大冲突面
- **外键约束**:父子表同时更新时的加锁顺序
- **`UPDATE` 无 WHERE 条件**:锁全表

4. **死锁监控与排查**:
- `SHOW ENGINE INNODB STATUS`:看 `LATEST DETECTED DEADLOCK` 段,包含两个事务的 SQL、持有的锁、等待的锁
- `information_schema.innodb_trx`:当前运行中的事务
- `information_schema.innodb_locks`(5.7)/ `performance_schema.data_locks`(8.0):当前持有的锁
- `performance_schema.data_lock_waits`(8.0):锁等待链,看谁等谁
- 慢查询日志里 `Error 1213` 频次监控

5. **业务侧防死锁方案(优先级从高到低)**:
- **固定加锁顺序**:所有事务按主键升序加锁,如统一先锁 id 小的行
- **缩小事务粒度**:事务内越少锁越好,避免长事务
- **减少事务内 SQL 数量**:一条 SQL 完成就一条,避免多次 SELECT FOR UPDATE + UPDATE
- **加锁前先用 SELECT 判断**:能用 `SELECT ... FOR UPDATE NOWAIT`(8.0)就别阻塞
- **走乐观锁**:用 `version` 列 CAS 更新,避免悲观锁冲突
- **合理建索引**:无索引的 UPDATE/DELETE 会锁大量行
- **控制并发**:热点行加应用层分布式锁(Redis、Zookeeper)串行化
- **调大 timeout**:`innodb_lock_wait_timeout` 从 50s 提到 300s,让慢事务有机会完成

6. **参数调整场景**:
- 默认 `innodb_deadlock_detect=ON`:适合事务短、数量少
- 大量长事务:检测开销大,可 `innodb_deadlock_detect=OFF` + 依赖 timeout
- 高并发写入:可能需 `innodb_lock_wait_timeout` 减小到 5-10 秒快速失败

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清 InnoDB 的自动死锁检测机制与牺牲者选择逻辑
- **广度**:能否覆盖监控工具链、参数调优、业务侧规避
- **应用**:能否在真实线上死锁事故中给出一套定位与修复方案

**延伸递进面试题**:
1. **为什么两个相同 SQL 并发执行会死锁,同一个 SQL 单独跑不会?**
- 解答要点:SQL 相同不代表访问顺序相同。RR 隔离下两条 UPDATE 可能锁不同的间隙/行,如果扫描顺序受 B+ 树遍历影响,可能形成交叉等待。
2. **`innodb_deadlock_detect=OFF` 有什么后果?**
- 解答要点:不再自动检测,只能依赖 `innodb_lock_wait_timeout` 让等锁方超时回滚。优点:省 CPU(大事务场景明显);缺点:恢复慢、等待时间长。
3. **怎么在压测环境主动触发死锁?**
- 解答要点:两个会话,会话 1 `BEGIN; UPDATE t SET c=c+1 WHERE id=1;`(不 commit),会话 2 `BEGIN; UPDATE t SET c=c+1 WHERE id=2;`,然后会话 1 `UPDATE t SET c=c+1 WHERE id=2;`,会话 2 `UPDATE t SET c=c+1 WHERE id=1;`,会话 1 立刻收到 1213 报错。

第三部分:面试者参考回答(口语化)

面试官您好,死锁我先从理论上讲清楚,再落到 InnoDB 的具体表现。

理论上死锁有四个必要条件:互斥、占有且等待、不可剥夺、循环等待。破坏任意一个就能防止死锁。经典例子是两个事务,A 拿了资源 1 等 2,B 拿了资源 2 等 1,就形成循环等待,谁都动不了。

但在 MySQL InnoDB 里,死锁的表现和检测方式跟理论不太一样,我主要讲几个点。

**InnoDB 的默认行为是自动检测**。`innodb_deadlock_detect` 默认开启,事务在等锁的时候会持续构建 wait-for graph,一旦发现环,就立刻选出牺牲者——通常是 undo log 少、改动少的那个事务,回滚它并抛 `ERROR 1213: Deadlock found`,另一事务继续执行。所以开发者看到的错误是 1213,业务代码要做重试。检测本身有 O(N²) 开销,事务特别多的场景可以考虑关闭检测,改用 `innodb_lock_wait_timeout`(默认 50 秒)来兜底——超时会抛 1205 错误。

**常见死锁场景**有几个。第一是间隙锁交叉,RR 隔离级别下两个事务用不同的 WHERE 条件触发不同间隙锁,最终循环等待。第二是多行更新顺序不同,A 先锁 id=1 再锁 id=2,B 反过来。第三是 `INSERT ... ON DUPLICATE KEY UPDATE`,并发插相同主键时一个走 INSERT 一个走 UPDATE 触发死锁。第四是 UPDATE 没写 WHERE 条件锁全表。第五是索引缺失导致锁大量行放大冲突面。

**排查线上死锁**我会按这个链路走:先跑 `SHOW ENGINE INNODB STATUS` 看 `LATEST DETECTED DEADLOCK` 段,里面会显示两个事务的 SQL、各自持有的锁、等待的锁;再看 `performance_schema.data_lock_waits` 找锁等待链;最后看 `innodb_trx` 找超长事务。慢查询日志里频繁出现 1213 错误就是死锁高发信号。

**预防方案**我按优先级列一下。第一是**固定加锁顺序**,所有事务按主键升序加锁,这是最有效的办法,业务侧只要约定统一顺序就能消掉大部分死锁。第二是**缩小事务粒度**,一条 SQL 能完成就别拆多条,事务内减少锁数量。第三是**避免长事务**,长事务持有锁越久,越容易和其他事务撞。第四是**加索引**,无索引的 UPDATE 会锁大量行。第五是**用乐观锁绕过**,`version` 列 CAS 更新,冲突就重试,不用悲观锁串行化。第六是**8.0 的 NOWAIT / SKIP LOCKED**,`SELECT FOR UPDATE NOWAIT` 拿不到锁立即返回,避免阻塞等待成环。

参数层面,默认配置基本够用,只是长事务多、检测开销大的场景可以 `innodb_deadlock_detect=OFF`,靠 `innodb_lock_wait_timeout` 兜底。反过来热点行死锁频发的场景,把 `innodb_lock_wait_timeout` 调小让失败快一点,业务侧快速重试。

https://chat.deepseek.com/share/wy9ppt0dw6b7uhgyct

---

📚 查看全部面试记录