M18. InnoDB 中意向锁(IS / IX)的作用是?
考点:意向锁
A. 用于行锁之间的互斥 B. 是一种表级锁,用于快速判断“表中是否存在行锁”,避免加表锁时逐行检查 C. 用于实现 MVCC 的可见性判断 D. 用于主从复制的一致性校验
答案:B
【考点】意向锁的定位:表级锁 + 兼容性矩阵 + 与行锁的关系。
【结论】 选 B —— 意向锁是 InnoDB 自动添加的表级锁,唯一作用是快速判断“表中是否已有行锁”,让加表锁时不必逐行检查。
【逐项辨析】
- A 用于行锁之间的互斥:错。行锁之间的互斥靠索引记录上的锁本身判断,与意向锁无关;意向锁是表级的。
- B 是一种表级锁,用于快速判断表中是否存在行锁,避免加表锁时逐行检查:正确。 定位(表级)、作用(快速判断)、价值(避免逐行扫描)三项全对。
- C 用于实现 MVCC 的可见性判断:错。可见性判断靠 ReadView + 隐藏列 + undo log(见 M12),意向锁不参与。
- D 用于主从复制的一致性校验:错。主从一致性靠 binlog 与两阶段提交(见 M16),与锁无关。
【知识点】 先明确意向锁是什么:意向锁(IS / IX)是表级锁,由 InnoDB 在给行加锁时自动添加,用户无法手动干预。
| 意向锁 | 全称 | 何时自动添加 |
|---|---|---|
| IS | 意向共享锁(Intention Shared) | 事务准备给某行加 S 锁(共享锁)之前 |
| IX | 意向排他锁(Intention Exclusive) | 事务准备给某行加 X 锁(排他锁)之前 |
它要解决的问题:假设事务 A 已给表中 3 行加了行锁,事务 B 想给整张表加表级 X 锁。若没有意向锁,B 必须逐行扫描全表才能确认“有没有行锁”,表越大越慢;有了意向锁,B 只要看一眼“这张表上有没有 IX / IS”就立即得到答案。意向锁就是行锁的“表级代言人”。
兼容性矩阵(✅ 兼容,❌ 冲突):
| 已持有 ↓ / 请求 → | IS | IX | 表级 S | 表级 X |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| 表级 S | ✅ | ❌ | ✅ | ❌ |
| 表级 X | ❌ | ❌ | ❌ | ❌ |
矩阵读法(三条结论):
- 意向锁之间全部互相兼容 —— IS / IX 只表示“我打算在某行上加锁”,多个事务可以同时在不同行上加锁。
- IS 与表级 S 兼容 —— 因为表级 S 与行级 S 方向一致,都是读。
- IX 与表级 S / X 都冲突 —— IX 意味着“表中有行要写”,与整表加读锁或写锁都无法共存。
推导:矩阵的实质是“表级锁必须与表内所有行级锁兼容才不冲突”。因为意向锁忠实地代表了“表中行锁的存在与类型”,所以判断“表锁能否加”只需检查意向锁,逻辑上等价于检查全表行锁。
【记忆锚点】 “意向锁是行锁的代言人,表锁问它就行” —— 意向锁之间不打架,只跟表锁打架。
【易混对比】
- 意向锁(IS / IX)vs 表级锁(S / X):都是表级,但意向锁是自动加的、仅作标记,不真正阻塞别人读写行;表级 S / X 才真正锁住整表。
- 意向锁 vs 行锁:行锁加在索引记录上(见 M17),意向锁加在表上;一次加行锁会连带加意向锁。
- IS vs IX:IS 对应“要加行 S 锁”,IX 对应“要加行 X 锁”;IX 更“霸道”,与表级 S 也冲突。
- 换问法:若题目问“事务要给某行加 X 锁,会先在表上加什么锁”,答案是 IX 锁。
【自测】 事务 A 执行 SELECT * FROM t WHERE id = 1 FOR UPDATE(id 是主键)。此时表 t 上会存在哪两种锁?
答:表级 IX 锁 + 主键索引记录上的 X 锁。先自动加 IX 表示“表内有行要写”,再在
id = 1的索引记录上加 X 锁。与 M11、M17 连考;大厂面试高频。
【知识关联】
- 补题关联:补-19(表锁与行锁的兼容检查)。
- 面试/工程:意向锁让
LOCK TABLES之类表级请求不必逐行检查;理解它能解释为什么“有行锁时整表读仍可能被挡住”。 - 面试追问:① IS 与 IX 互相兼容吗? ② 意向锁会阻塞普通
SELECT吗?
【拓展延伸】
- 变式问法:问“意向锁存在的唯一目的是什么”(快速判断表级与行级冲突)。
- 参数/命令:
SELECT * FROM performance_schema.data_locks WHERE lock_type='TABLE'可看到 IS/IX;LOCK TABLE t READ/WRITE会触发表级与意向锁交互。
M19. EXPLAIN 结果的 type 字段中,访问效率最好的是?
考点:EXPLAIN 的 type 字段
A. ALL(全表扫描) B. index(全索引扫描) C. range(范围扫描) D. const(通过主键或唯一索引等值查询,最多一行)
答案:D
【考点】EXPLAIN 中 type 字段的访问类型与优劣顺序。
【结论】 选 D —— type 效率从高到低大致为 system > const > eq_ref > ref > range > index > ALL,其中 const 通过主键或唯一索引等值查询最多命中一行,效率最高。
【逐项辨析】
- A
ALL全表扫描:错。这是最差的访问类型,是首要优化信号,不是最好。 - B
index全索引扫描:错。它比ALL略好(扫的是更小的索引文件,且通常有序),但仍需扫完整棵索引树,属需要优化的类型。 - C
range范围扫描:错。range已能用索引定位区间,是可接受的类型,但仍不如const精准。 - D
const通过主键或唯一索引等值查询,最多一行:正确。 优化阶段就能确定结果集最多一行,访问代价最低。
【知识点】 type 表示“MySQL 决定用什么方式访问这张表”,是 EXPLAIN 中最该先看的一列。
type | 含义 | 典型场景 | 评价 |
|---|---|---|---|
system | 表只有一行(系统表) | 极少见 | 最好 |
const | 通过主键 / 唯一索引等值查询,最多一行 | WHERE id = 5 | 最好(实用场景) |
eq_ref | 被驱动表通过唯一索引等值匹配,一行 | 多表 JOIN 中被驱动表走主键 | 很好 |
ref | 通过普通索引等值匹配,可能多行 | WHERE name = 'a'(name 为普通索引) | 好 |
range | 范围扫描索引 | BETWEEN、>、<、IN | 可接受 |
index | 全索引扫描 | 扫完整个索引树 | 需优化 |
ALL | 全表扫描 | 无可用索引 | 最需优化 |
推导:效率的本质是“要读多少数据、能不能精确定位”。const 在优化阶段就能借助唯一性约束推断出“最多一行”,连多读一次都不必;ref 只能确定“一批行”,要到执行阶段才知道有多少;range 要遍历区间内的所有索引项;index 要遍历整棵索引树;ALL 要遍历整张表的所有数据页 —— 从“精确一行”到“全表所有行”,扫描量单调递增,效率单调递减。
其他常见取值:ref_or_null(ref + 额外查 NULL)、index_merge(索引合并)、unique_subquery / index_subquery(子查询优化)、fulltext(全文索引)。
【记忆锚点】 "system > const > eq_ref > ref > range > index > ALL" —— 背下这条链,看 EXPLAIN 先对号入座。
【易混对比】
constvseq_ref:const是单表查询最多一行;eq_ref是多表 JOIN 中被驱动表每匹配一行。二者都基于唯一索引等值,区别在单表 / 关联。refvsrange:ref是等值匹配(可能多行),range是范围匹配(区间)。indexvsALL:都“扫全”,区别是index只扫索引树(更小、通常有序),ALL扫数据页。- 换问法:若题目问“哪个
type说明需要重点优化”,答案是ALL(其次index)。
【自测】 表 t 有主键 id 与普通索引 name。EXPLAIN SELECT * FROM t WHERE name = 'a' 的 type 是什么?改成 WHERE id = 5 呢?
答:前者是
ref(普通索引等值,可能多行);后者是const(主键等值,最多一行)。与 M20 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(ICP 时 type 可能是 range/ref,Extra 才是关键)。
- 面试/工程:SQL 优化第一眼就是 type 与 key;
ALL/index大范围出现意味着索引设计或写法失败。 - 面试追问:①
type=range一定比ref慢吗? ② 为什么index(全索引扫描)有时仍可接受?
【拓展延伸】
- 变式问法:给一串 type 要求排序:
system > const > eq_ref > ref > range > index > ALL。 - 参数/命令:
EXPLAIN FORMAT=TREE(8.0.16+)/EXPLAIN ANALYZE更直观;optimizer_trace可看为何没选更好 type。
M20. EXPLAIN 的 Extra 中出现 Using filesort 说明?
考点:EXPLAIN 的 Extra 字段
A. 使用了磁盘临时文件排序,是需要优化的信号 B. 属于正常现象,无需关注 C. 说明已经利用了索引的有序性完成排序 D. 说明使用了覆盖索引
答案:A
【考点】Extra 中常见提示(Using filesort / Using temporary / Using index / Using where)的含义与优化方向。
【结论】 选 A —— Using filesort 说明 MySQL 无法利用索引的有序性、需要额外做一次排序,是需要优化的信号。
【逐项辨析】
- A 使用了磁盘临时文件排序,是需要优化的信号:正确。 严格口径是:
Extra里出现Using filesort表示本次查询发生了 filesort —— 索引的有序性用不上,MySQL 必须额外做一次排序,因此是需要优化的信号。至于“磁盘临时文件”:排序先在sort_buffer_size的内存缓冲区里进行,只有数据量放不下时才落盘做归并排序,所以落盘是最坏情况而非必然动作(详见下文澄清)。本题仍选 A 的理由:四个选项里只有 A 命中“额外排序 + 需要优化”这一核心语义,其余三项都与Using filesort直接矛盾(B 说无需关注、C 说已用上索引有序性、D 说覆盖索引),按“最优选项”原则取 A。 - B 属于正常现象、无需关注:错。它意味着多了一次排序开销,是明确的优化信号,不能说“无需关注”。
- C 说明已经利用了索引的有序性完成排序:错。恰恰相反 —— 如果索引有序性够用,
Extra里根本不会出现Using filesort。 - D 说明使用了覆盖索引:错。覆盖索引的标志是
Using index,与排序无关。
【知识点】 Extra 列是 EXPLAIN 的“诊断意见”,比 type 更能指出具体的性能病灶。四个高频值对照:
Extra | 含义 | 好坏 | 优化方向 |
|---|---|---|---|
Using filesort | 无法用索引有序性,额外做一次排序 | ❌ 需优化 | 为 WHERE + ORDER BY 建联合索引,让索引天然有序 |
Using temporary | 使用了临时表 | ❌ 需优化 | 常见于 GROUP BY / DISTINCT 未走索引,补索引 |
Using index | 用到了覆盖索引,无需回表 | ✅ 好事 | 保持,这是理想状态 |
Using where | 需要在取到行之后再过滤 | ⚠️ 中性 | 视情况把过滤条件下推到索引 |
关于 Using filesort 名称的关键澄清:它不一定真的用磁盘文件。MySQL 会先在内存中排序(缓冲区大小由 sort_buffer_size 决定),只有数据量超出排序缓冲区时才退化为磁盘临时文件归并排序。所以 filesort 应当理解为“额外的一次排序操作”,而非“一定落磁盘”。
Using filesort 的三条常见成因:
ORDER BY的列与索引的顺序不匹配(例如索引是(a, b),却按b排序);- 排序方向不一致(例如索引是
(a ASC, b ASC),却ORDER BY a ASC, b DESC,MySQL 8.0 之前无法直接利用); - 排序列未包含在本次使用的索引中。
推导:B+ 树索引本身天然有序(叶子节点按键值顺序用双向链表相连),所以只要 ORDER BY 的列顺序与索引顺序完全对齐,MySQL 只需顺着索引叶子链表读就能拿到有序结果,无需再排。一旦对不齐,索引的有序性就“用不上”,只能把结果集取出来重新排一遍 —— 这就是 Using filesort 的由来。
【记忆锚点】 “Using index 是奖状,Using filesort / Using temporary 是病危通知” —— 一个说“省了回表”,两个说“多了一道开销”。
【易混对比】
Using filesortvsUsing temporary:前者多一次排序,后者多建一张临时表;GROUP BY走不了索引时两者常同时出现。Using indexvsUsing where:Using index表示“索引里就有全部所需列”(覆盖索引,好事);Using where表示“存储引擎返回后还要再过滤”(中性)。Using indexvsUsing index condition:前者是覆盖索引(不回表),后者是索引条件下推(ICP),仍可能回表,切勿混为一谈。- 换问法:若题目问“
Extra中出现哪个值说明用到了覆盖索引”,答案是Using index。
【自测】 表 t 有联合索引 idx(a, b)。SELECT a, b FROM t ORDER BY a, b 与 SELECT a, b FROM t ORDER BY b 的 Extra 分别是什么?
答:前者不出现
Using filesort(排序键与索引顺序(a, b)一致,且两列都在索引中,会出现Using index);后者出现Using filesort(排序键只有b,与索引顺序不对齐,用不上有序性)。与 M19 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(Extra 字段家族辨析)。
- 面试/工程:
Using filesort不一定是“磁盘排序”,内存排序也会这么显示;优先消除的是“无法用索引有序”的根因。 - 面试追问:①
Using temporary与Using filesort常一起出现吗? ② 如何消掉 filesort?
【拓展延伸】
- 变式问法:把 Extra 选项改成
Using index/Using where/Using index condition互考。 - 参数/命令:
sort_buffer_size、max_length_for_sort_data;ORDER BY借索引时 Extra 无 filesort;EXPLAIN ANALYZE可见 actual sort time。
M21. 开启并配置 MySQL 慢查询日志,涉及的参数是?
考点:慢查询日志
A. slow_query_log 与 long_query_time B. binlog_format C. innodb_buffer_pool_size D. max_connections
答案:A
【考点】慢查询日志的开启方式与配套参数。
【结论】 选 A:slow_query_log 管开关、long_query_time 管阈值,一开一卡,超过阈值的语句才会被记进慢查询日志。
【逐项辨析】
- A
slow_query_log与long_query_time:正确。 前者是慢查询日志的总开关(ON/OFF),后者是“超过多少秒算慢”的阈值(单位秒,支持小数如0.2),两者配套使用才生效。 - B
binlog_format:错。它管的是 binlog 的记录格式(STATEMENT/ROW/MIXED),决定的是“变更怎么记”,与“慢语句要不要记”无关。 - C
innodb_buffer_pool_size:错。这是 InnoDB 缓冲池大小,属于性能调优参数,改它不影响慢查询日志是否开启。 - D
max_connections:错。这是允许的最大并发连接数,属连接层参数,与慢查询日志没有任何关系。
【知识点】 慢查询日志是 MySQL 的“慢 SQL 侦查器”:一条语句的实际执行时间超过 long_query_time,MySQL 就把它的原文、耗时、扫描行数、返回行数等写进日志文件,供事后分析。
| 参数 | 作用 | 典型取值 |
|---|---|---|
slow_query_log | 慢查询日志总开关 | ON / OFF |
slow_query_log_file | 日志文件路径 | /var/log/mysql/slow.log |
long_query_time | 阈值(秒,支持小数) | 0.2 / 0.5 / 1 |
log_queries_not_using_indexes | 额外记录未走索引的语句 | 线上慎开(量极大) |
log_output | 输出到文件还是表 | FILE / TABLE |
生产排查是一条固定流水线:慢查询日志 → mysqldumpslow / pt-query-digest 汇总排序 → 对 TOP 慢 SQL 执行 EXPLAIN 看执行计划 → 加索引或改写 SQL。两个容易踩的点:一是 long_query_time 默认值通常为 10 秒,默认阈值偏高,线上一般调到 0.2~1 秒才抓得住问题;二是 log_queries_not_using_indexes 虽然能捞到未走索引的语句,但在大表上会瞬间刷爆日志文件,只建议短时排查时临时打开。
【记忆锚点】 “开日志看 slow_query_log,定快慢看 long_query_time” —— 一个管开关,一个管标尺。
【易混对比】
- 慢查询日志 vs binlog:慢查询日志面向“性能排查”(记执行慢的语句);binlog 面向“数据复制与恢复”(记所有变更)。两者目的完全不同,
binlog_format管不了慢查询。 long_query_timevslog_queries_not_using_indexes:前者按时间筛,后者按是否走索引筛,排查时通常两者一起看。- 换问法:若题目问“记录未使用索引的语句用哪个参数”,答案就变成
log_queries_not_using_indexes;若问“慢查询日志写到哪个文件”,答案是slow_query_log_file。
【自测】 某线上库想抓出所有执行时间超过 200 毫秒的 SQL,除 slow_query_log = ON 外,还要把哪个参数设成什么值?
答:
long_query_time = 0.2。该参数单位是秒且支持小数,200 毫秒即 0.2 秒。与 M19、M20 同属日志与排查链路;DBA 岗与运维面试高频。
【知识关联】
- 补题关联:补-25(慢 SQL 分析常从 type/Extra/ICP 入手)。
- 面试/工程:慢查询日志 +
mysqldumpslow/pt-query-digest是 DBA 日常;应用侧还应配合 APM 采样,避免只看库不看调用方。 - 面试追问:①
long_query_time=0有什么风险? ② 慢日志里的 Rows_examined 远大于 Rows_sent 说明什么?
【拓展延伸】
- 变式问法:问“只记录未走索引的 SQL”用什么参数;问“如何动态开启不用重启”。
- 参数/命令:
slow_query_log、slow_query_log_file、long_query_time、log_queries_not_using_indexes、min_examined_row_limit;注意开关参数没有long_query_log这个名字 —— 5.6 之前叫log_slow_queries,5.6 起统一为slow_query_log;8.0 支持SET GLOBAL slow_query_log=ON热更新。
M22. InnoDB 建议使用自增主键的原因,不包括?
考点:主键选型
A. 自增主键顺序写入,减少页分裂与数据移动,插入性能更好 B. 自增主键占用空间小,而二级索引叶子要存主键值,因此索引更省空间 C. 自增主键天然唯一,避免业务键冲突 D. 自增主键可以避免回表
答案:D
【考点】主键选型对插入性能与索引体积的影响。
【结论】 选 D。回表与否只取决于“查询需要的列是否都在所用索引里”,与主键是否自增毫无关系;A、B、C 都是自增主键的真实优势,因此“不包括”的正是 D。
【逐项辨析】
- A 自增主键顺序写入、减少页分裂与数据移动:是优势,不选。新记录总落在 B+ 树最右侧页,几乎不触发结构调整,插入是顺序 IO。
- B 自增主键占用空间小、二级索引更省空间:是优势,不选。二级索引叶子要存主键值,主键越短,每个索引页能装的条目越多,树越矮。
- C 自增主键天然唯一、避免业务键冲突:是优势,不选。自增值由数据库生成、不含业务语义,业务规则变更也不会牵动主键。
- D 自增主键可以避免回表:错误命题。 回表的成因是“二级索引叶子只有索引列 + 主键,缺其余列”,与主键是否自增无关 —— 用自增主键去查非索引列,一样要回表。
【知识点】 主键选型的三条判据:有序性、长度、业务无关性。
| 主键类型 | 写入有序性 | 长度 | 二级索引体积 | 典型问题 |
|---|---|---|---|---|
自增 BIGINT | 顺序追加,几乎不页分裂 | 8 字节 | 最小 | 主键值可被外部枚举 |
| 无序 UUID(v4) | 随机插入,频繁页分裂 | 36 字节字符串 | 膨胀数倍 | 写放大、碎片多、命中率低 |
| 有序 UUID(v7 / 雪花 ID) | 近似顺序 | 16 字节或更长 | 较大 | 实现复杂、依赖时钟 |
| 业务键(手机号 / 身份证) | 随机 | 较长 | 较大 | 业务一变就要改主键 |
关键推导有两条。其一(写性能):InnoDB 的聚簇索引按主键有序组织,新主键若总是当前最大值,新行就追加到最右页,写满直接申请新页;若主键随机,新行要插进已有页的中间,页满时触发页分裂(一页拆两页、搬移约一半数据),同时产生空间碎片与随机 IO。其二(读性能):二级索引叶子存的是“索引列 + 主键值”,主键每长 1 字节,二级索引的每个条目就长 1 字节;主键从 8 字节 BIGINT 换成 36 字节 UUID,二级索引体积膨胀约 4 倍以上(口径:按窄索引列估算 —— 实算主键 1 字节时 4.11 倍、10 字节 2.56 倍、20 字节 2.00 倍,列越宽倍数越小),同样的缓冲池只能缓存更少的索引页,命中率随之下降。结论:自增 BIGINT 是 InnoDB 主键的默认最优解;必须用 UUID 时优先选有序 UUID。
【记忆锚点】 “自增主键三好处:写得顺(有序)、占得少(短)、管得省(无业务含义)” —— 回表不在这三条里。
【易混对比】
- 自增主键的优势 vs 回表:前者讲“写入与索引体积”,后者讲“查询能否只查一棵树”,两者维度不同。本题的陷阱正是把这两件事硬扯上因果关系。
- 自增主键 vs 业务主键:自增主键的缺点是可被外部枚举、分库分表后需改造(改用雪花 ID);业务主键的缺点是业务一变更就要改主键,代价极大。
- 换问法:若题干改成“下列哪项是自增主键的优势”,答案就是 A / B / C 中任意一项;若问“二级索引叶子存什么”,答案仍是“索引列 + 主键值”(与 M01 互为镜像)。
【自测】 某表原用自增 BIGINT 主键,后改用 UUIDv4 字符串做主键,结果二级索引文件明显变大、插入 TPS 下降。这两个现象各自的直接原因是什么?
答:索引变大是因为二级索引叶子要存主键值,主键由 8 字节变 36 字节;插入变慢是因为 UUIDv4 随机无序,新行插在已有页中间,触发频繁页分裂与随机 IO。与 M01、M23 连考;大厂面试高频。
【知识关联】
- 补题关联:补-23(死锁检测与主键冲突场景)、补-24(金额用 DECIMAL,类型选型与主键长度同属“类型代价”)。
- 面试/工程:分库分表后自增失效,常见雪花/号段发号器;对外暴露自增 ID 可被竞品枚举订单量,需混淆(雪花、加密 ID)。主库高并发写入时,自增近似顺序追加,UUID 则把写放大打满磁盘 IOPS——压测里同一业务换主键 TPS 可差数倍。
- 面试追问:① 雪花 ID 会不会页分裂?(会,但远少于 UUIDv4:时间戳高位大致递增,只有同毫秒内或时钟回拨才可能乱序) ② 为什么二级索引更怕长主键?(每个二级索引叶子都要冗余一份主键,主键从 8B→16B,所有二级索引体积与 Buffer Pool 占用同步放大)
【拓展延伸】
- 变式问法:正向问“InnoDB 主键推荐”→ 自增 BIGINT;反向问“自增主键缺点”→ 可枚举、分库分表难扩展、无法做业务语义。把“手机号当主键”设为错误项(业务变更+长度+随机)。
- 参数/命令:
innodb_autoinc_lock_mode(0 传统 / 1 连续 / 2 交错,影响批量插入与 statement 复制安全);SHOW CREATE TABLE看主键定义;UUIDv7/雪花可存BINARY(16)而非CHAR(36)省一半空间;压测对比时用performance_schema看 page splits。
M23. 使用无序的 UUID 作为 InnoDB 主键,最主要的缺点是?
考点:页分裂
A. 无序插入导致频繁页分裂与数据移动,插入性能差;且长度长,二级索引占用空间更大 B. UUID 不保证唯一性 C. UUID 列不能建索引 D. UUID 无法参与排序
答案:A
【考点】页分裂的产生原因及其对写性能的影响。
【结论】 选 A:无序 UUID 会让新记录插到任意页中间,页满即页分裂并搬移约一半数据;叠加 36 字节的长度,二级索引也被撑大。
【逐项辨析】
- A 无序插入导致频繁页分裂与数据移动、插入性能差;且长度长、二级索引占用空间更大:正确。 一句话把“写放大”与“空间膨胀”两个真实代价都点到了。
- B UUID 不保证唯一性:错。唯一性恰恰是 UUID 的强项,它的短板是随机无序与长度长,与唯一性无关。
- C UUID 列不能建索引:错。UUID 列完全可以建索引,只是索引体积更大、写入更慢 —— 把“代价高”说成了“做不到”。
- D UUID 无法参与排序:错。UUID 是字符串,可比较、可排序,
ORDER BY照常工作,只是排序结果按字典序而非时间序。
【知识点】 页分裂的根源是“B+ 树要求页内有序”,是否分裂取决于插入位置是否连续。
| 主键形态 | 新记录落点 | 是否触发页分裂 | IO 特征 |
|---|---|---|---|
自增 BIGINT | 最右页尾部追加 | 仅在页满时申请新页 | 顺序 IO |
| 无序 UUID | 任意页中间随机插入 | 频繁分裂(拆页 + 搬移约一半数据) | 随机 IO |
推导链条:聚簇索引的叶子页内部按键值升序排列 → 插入一条“键值落在中间”的记录,必须塞进对应位置 → 该页写满时 InnoDB 只能把一页拆成两页并重新分配记录(通常各占约一半)→ 拆分后两页都只填了一半,空间利用率下降、碎片增多,同时被拆页及其父节点都要落盘,产生额外的随机写。因此 UUID 表的典型症状是:写入 TPS 下降、表空间虚高、缓冲池命中率下降。补两个口径:一是有序 UUID(UUIDv7 / 雪花 ID)能大幅缓解但无法完全消除;二是自增主键并非绝对免疫,若删掉中间一段再随机回插,同样会触发页分裂。
【记忆锚点】 “自增往后加,UUID 到处插;插到页中间,一页变两页”。
【易混对比】
- 页分裂 vs 页合并:分裂由“页写满 + 中间插入”触发;合并由“大量删除后页利用率过低”触发。二者是方向相反的动作。
- UUIDv4 vs UUIDv7 / 雪花 ID:前者完全随机、必然页分裂;后者时间有序、接近顺序追加,是“用 UUID 又不吃页分裂亏”的主流方案。
- 换问法:若题目问“为什么推荐
BIGINT而不是 UUID 做主键”,答案仍是“顺序写入减少页分裂 + 长度短使二级索引更小”(与 M22 是同一知识点的正反两面)。
【自测】 一张日志表主键是自增 BIGINT,日常只追加写入、性能良好。某天业务改为“按时间回补历史数据”,随机插入大量旧时间戳记录,写性能明显恶化。原因是什么?
答:回补的旧记录键值落在已有页中间,破坏了“顺序追加”的前提,触发频繁页分裂与随机 IO。缓解方式是按时间分区或批量顺序回填。与 M22 连考;DBA 岗高频。
【知识关联】
- 补题关联:补-22(页被载入 Buffer Pool,分裂增加 IO)。
- 面试/工程:导入历史数据时先关索引再开,或按主键有序落地;监控
Innodb_row_inserts与磁盘写放大可侧面看到分裂代价。 - 面试追问:① 页分裂与页合并分别何时触发? ② 有序 UUID 能否完全避免分裂?
【拓展延伸】
- 变式问法:问“为什么日志表突然变慢”——常是随机回补导致分裂。
- 参数/命令:
INFORMATION_SCHEMA.INNODB_METRICS的index_page_splits(本机 8.0.46 实测:NAME LIKE '%split%'只有这一条;状态变量里没有Innodb_buffer_pool_pages_split——Innodb_buffer_pool_pages_*只有 data/dirty/flushed/free/misc/total 六项);页填充因子相关实践(预留空间减少分裂);innodb_page_size影响一页能装多少行。
M24. SELECT * FROM t ORDER BY id LIMIT 1000000, 10 执行很慢,合理的优化思路是?
考点:深分页优化
A. 再多建几个索引就能解决 B. 采用延迟关联(先用覆盖索引取出主键,再 JOIN 回原表)或基于游标翻页(WHERE id > 上次最大 id LIMIT 10) C. 加上 ORDER BY 就能加速 D. 深分页无法优化
答案:B
【考点】深分页的性能瓶颈与两种标准解法。
【结论】 选 B。LIMIT 1000000, 10 的瓶颈是必须先扫过并丢弃前 1000000 行,解法就是“让这 100 万行不必回表”——延迟关联或游标翻页。
【逐项辨析】
- A 再多建几个索引就能解决:错。多建索引改变不了“要跳过 N 行”这个动作本身,甚至因索引更多、优化器选择变差而更慢;问题出在偏移量,不在索引数量。
- B 延迟关联或基于游标翻页:正确。 延迟关联把“排序 + 偏移”压进覆盖索引内完成,只对最终 10 行回表;游标翻页把偏移量换成范围条件,彻底绕开“扫描丢弃”。
- C 加上
ORDER BY就能加速:错。原语句本来就有ORDER BY id,再加没有意义;ORDER BY本身也不是加速手段,能否加速取决于排序能否用上索引的有序性。 - D 深分页无法优化:错。属于“放弃治疗”式表述,B 的两种方案都是成熟工程实践。
【知识点】 深分页慢的本质是偏移量被当成了“要扫过的行数”。
LIMIT m, n 的执行语义不是“直接跳到第 m 行”,而是“先取前 m + n 行,再丢掉前 m 行,返回剩下 n 行”。若查询走的是二级索引,被丢掉的那 100 万行每一行都要回表取 * 所需的列,于是 100 万次随机 IO 白做。两种解法正是针对这两点分别下刀:
| 方案 | 写法要点 | 优化掉的是什么 | 代价 |
|---|---|---|---|
| 延迟关联 | 子查询只在覆盖索引里 ORDER BY ... LIMIT m, n 取主键,外层 JOIN 回表取整行 | 把“100 万次回表”降为只有最终 n 行回表 | 仍需扫描并丢弃前 m 行(但只扫索引,代价小得多) |
| 游标 / 书签翻页 | 记住上一页最大 id,改成 WHERE id > 上次最大 id ORDER BY id LIMIT n | 把“偏移量”换成范围查询,不再扫描丢弃 | 不支持随机跳页,只能上一页 / 下一页 |
延迟关联的完整形态:
SELECT t.* FROM t
JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000, 10) AS x USING (id);子查询只需要 id 一列,而 id 恰好在主键索引(聚簇索引)上,因此这一步是覆盖索引扫描、不回表;外层 JOIN 只对 10 个 id 回表。游标翻页则把复杂度从 O(m + n) 降到 O(log N + n)(借助 B+ 树定位起点)。选型建议:C 端列表页(只能上一页 / 下一页)用游标翻页;后台管理页(需要跳页)用延迟关联。
【记忆锚点】 “深分页慢在”白扫一大片“——延迟关联让扫描不回表,游标翻页让扫描不存在”。
【易混对比】
- 延迟关联 vs 游标翻页:前者保留跳页能力、性能改善有限;后者性能最好但只能顺序翻页。二者是“能力与性能”的取舍。
- 深分页 vs
ORDER BY无索引:两者都可能慢,但成因不同 —— 深分页慢在偏移量大(即使有索引也慢),无索引排序慢在排序本身(加合适索引即可解决)。本题ORDER BY id已走主键,属前者。 - 换问法:若题目问“必须支持跳页时怎么优化”,答案就是延迟关联;若问“只支持下一页时最优解”,答案就是游标翻页。
【自测】 表 t 有 500 万行,业务必须支持跳页。SELECT * FROM t ORDER BY id LIMIT 4000000, 20 很慢,改写成延迟关联后明显变快。请说明“变快”的直接原因。
答:排序与偏移 400 万行的动作被压进了覆盖索引(只需
id一列、不回表),只有最终 20 行回表取整行,省掉了约 400 万次随机回表。与 M19、M20 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(ICP 在索引层过滤,与延迟关联同属“少回表”)、补-22(Buffer Pool 命中率受索引/扫描方式影响)。
- 面试/工程:后台导出、报表深分页是慢查询 TOP 来源;C 端“加载更多”几乎都应改成游标。电商订单列表若仍
LIMIT 1000000,10,DBA 会直接打回。 - 面试追问:① 延迟关联为什么仍可能扫很多索引页?(前 m 行仍要在覆盖索引里顺序扫过并丢弃,只是不回表) ② 游标翻页如何保证不丢不重?(依赖稳定排序键,通常用主键或 (create_time,id) 复合游标,避免同时间戳抖动)
【拓展延伸】
- 变式问法:题干给出
LIMIT 1000000,10问优化方案;或问“为什么WHERE id > 1000000 LIMIT 10比LIMIT 1000000,10快”——前者走主键范围定位,复杂度 O(log N + n)。 - 参数/命令:
EXPLAIN看rows与Extra(Using index vs Using filesort);延迟关联模板见知识点;游标字段建议唯一且递增;若必须跳页,可粗分桶(按日期分区)再页内精定位。
M25. 关于 COUNT 函数,下列说法正确的是?
考点:MySQL 的 count
A. COUNT(*) 会跳过 NULL 行,COUNT(列) 统计所有行 B. COUNT(*)、COUNT(1)、COUNT(主键) 三者在语义上完全等价 C. COUNT(列) 会跳过该列为 NULL 的行,COUNT(*) 统计所有行 D. COUNT(1) 会跳过 NULL
答案:C
【考点】COUNT 的语义差异与 InnoDB 的计数优化。
【结论】 选 C。COUNT(列) 数的是“该列非 NULL 的行数”,所以跳过 NULL;COUNT(*) 数的是行数,与列值无关。
【逐项辨析】
- A
COUNT(*)会跳过NULL行、COUNT(列)统计所有行:错。方向恰好说反 —— 数所有行(因而不受 NULL 影响)的是COUNT(*),跳过 NULL 的是COUNT(列)。 - B
COUNT(*)、COUNT(1)、COUNT(主键)三者语义完全等价:错。表述过于绝对 ——COUNT(*)与COUNT(1)确实等价(都数行),而COUNT(主键)严格说是“数主键非 NULL 的行数”,只因主键不允许为 NULL,结果才恰好一致。 - C
COUNT(列)跳过该列为 NULL 的行,COUNT(*)统计所有行:正确。 这正是两者的分水岭。 - D
COUNT(1)会跳过 NULL:错。COUNT(1)里的1是常量,根本不涉及列值判断,不存在“跳过 NULL”一说。
【知识点】 聚合函数对 NULL 的处理规律 + InnoDB 的计数实现。
| 写法 | 数的是什么 | 是否受 NULL 影响 | 说明 |
|---|---|---|---|
COUNT(*) | 行数 | 否 | 优化器专门处理,选最小的可用索引遍历 |
COUNT(1) | 行数 | 否 | 与 COUNT(*) 结果相同,语义等价 |
COUNT(列) | 该列非 NULL 的个数 | 是,跳过 NULL | 列可空时结果小于总行数 |
COUNT(主键) | 主键非 NULL 的个数 | 否(主键非空) | 结果等于行数,但语义上仍走“判空” |
推导要抓两点。其一(语义):COUNT(expr) 的规则是“对每一行求 expr,结果不为 NULL 则计数加一”;COUNT(*) 是这条规则的特例 —— 它不针对任何列,直接数行。其二(实现):InnoDB 不像 MyISAM 那样保存行数(MyISAM 是表级锁、无 MVCC,可以存一个计数器;InnoDB 有 MVCC,同一时刻不同事务看到的行数可能不同,无法缓存统一值),因此 COUNT(*) 必须真的去扫。优化器的做法是选一棵最小的二级索引来遍历(二级索引叶子比聚簇索引小,扫的页更少),这也解释了“COUNT(*) 通常比 COUNT(某个可空列) 更快” —— 后者必须读列值判空,用不上这个优化。
补两个工程口径:一是超大表求总数,COUNT(*) 依然要扫全索引,建议改用近似值(EXPLAIN 的 rows 估算)、Redis 计数器或单独维护计数表;二是 COUNT(*) 与 COUNT(1) 在 MySQL 8.0 中已被优化到基本一致,不必纠结用哪个。
【记忆锚点】 “COUNT(*) 和 COUNT(1) 数行不挑食;COUNT(列) 数值挑食,NULL 一律不算”。
【易混对比】
COUNT(*)vsCOUNT(列):前者数行、含 NULL 行;后者数值、跳 NULL 行。COUNT(1)vsCOUNT(列):前者不读列、可走最小索引;后者必须读列判空,通常更慢。- NULL vs 空字符串:
''是“已知的空”,会被COUNT(列)计入;NULL 是“未知”,不计入。二者在 SQL 里语义完全不同。 - 换问法:若题干改成“某列有 3 行为 NULL、表共 10 行,求
COUNT(该列)”,答案就是 7(与补-09 是同型计算题)。
【自测】 表 t 共 100 万行,列 email 有 5 万行为 NULL。分别求 COUNT(*) 与 COUNT(email),并说明 COUNT(email) 为什么通常更慢。
答:
COUNT(*)= 1000000;COUNT(email)= 950000。COUNT(email)更慢是因为它必须读取
【知识关联】
- 补题关联:补-09(COUNT(*) 与 COUNT(col)/NULL 语义)。
- 面试/工程:大表
COUNT(*)会拖垮主库,应近似值(表统计信息)或异步计数表;分页总数可缓存。 - 面试追问:①
COUNT(1)与COUNT(*)等价吗? ②COUNT(主键)与COUNT(*)谁可能更慢?
【拓展延伸】
- 变式问法:给含 NULL 的表算
COUNT(*)/COUNT(col)/COUNT(DISTINCT col)。 - 参数/命令:
SHOW TABLE STATUS的Rows是估算;EXPLAIN对COUNT(*)有时给 rows 估算;可用触发器/应用维护精确计数。
M26. char(10) 与 varchar(10) 的区别,正确的是?
考点:char 与 varchar
A. char 定长,不足部分用空格补齐;varchar 变长,需额外 1~2 字节记录实际长度 B. 两者完全相同,只是写法不同 C. varchar 是定长,char 是变长 D. char 的最大长度只能是 255 个字符
答案:A
【考点】定长与变长字符类型的存储差异及适用场景。
【结论】 选 A:char 定长、不足部分用空格补齐;varchar 变长、额外用 1~2 字节记录实际长度。
【逐项辨析】
- A
char定长不足补空格,varchar变长需额外 1~2 字节记长度:正确。 这一句把两者的存储差异说全了。 - B 两者完全相同、只是写法不同:错。存储方式根本不同(定长 vs 变长),一个补空格、一个记长度,二者不可互换。
- C
varchar是定长、char是变长:错。把两者的定义完全说反。 - D
char的最大长度只能是 255 个字符:错在答非所问 —— 本题问的是char(10)与varchar(10)的存储差异,D 没有回答差异。(补充口径:CHAR的声明上限确实是 255 个字符,与字符集无关;utf8mb4 下CHAR(255)占 1020 字节,仍可正常建表,只是它会吃掉整行 65535 字节上限里的一大块。别把「字节数放大」误记成「字符数上限变小」。这一点在 5.7/8.0 一致。)
【知识点】 定长与变长的存储模型差异。
| 类型 | 存储方式 | 长度开销 | 尾部空格 | 适用场景 |
|---|---|---|---|---|
char(n) | 定长,永远占 n 个字符的位置 | 无额外字节 | 不足补空格(读取时去掉尾部空格) | 长度固定或变化极小:性别、手机号、邮编、MD5、定长 UUID |
varchar(n) | 变长,按实际长度存 | 额外 1~2 字节记长度 | 不补空格,原样保存 | 长度差异大:姓名、地址、备注、URL |
varchar 的长度前缀规则是:按该列定义的最大可能字节数判定 —— ≤ 255 用 1 字节,> 255 用 2 字节(最大可能字节数 = 声明字符数 × 字符集单字符最大字节数)。两点要抓住:① 判据是字节数而非字符数;② 是声明推出的上限而非某行实际长度,因为同一列每行前缀宽度必须一致。utf8mb4 下 varchar(100) 上限 400 字节 > 255,故恒为 2 字节前缀;latin1 下才是 1 字节。
选型逻辑:char 的优势是定长 —— 行内偏移可直接算出,读取不必先解长度、更新也不会因变长而挪动后续记录、不易产生碎片;代价是浪费空间(存一个“男”也要占满 10 个字符)。varchar 的优势是省空间;代价是更新时若长度变长,可能触发页内记录搬移。还有一个易忽略的语义差异:char 会去掉尾部空格,varchar 会保留,做等值比较时这一点可能造成结果不同。
【记忆锚点】 “char 定长补空格,varchar 变长带长度” —— 判据是“长度是否固定”,不是“哪个更省空间”。
【易混对比】
charvsvarchar:定长 vs 变长;补空格 vs 记长度;长度固定列 vs 长度不定列。- 字符数 vs 字节数:
varchar(100)的 100 是字符数,但长度前缀按字节数判断,两者不能混。 char去尾空格 vsvarchar保留空格:写 3 个空格进varchar就存 3 个;写进char会被并入补齐逻辑并在读取时去除。- 换问法:若题目问“
varchar(100)的长度前缀占几个字节”,判据是该列的最大可能字节数(声明字符数 × 字符集单字符最大字节数)是否超过 255,而不是某一行实际存了多少字节——同一列每行的前缀宽度必须一致,否则记录无法解析。utf8mb4 下 100×4=400>255 → 恒为 2 字节;latin1 下 100×1=100≤255 → 1 字节。
【自测】 列 code 固定存 32 位十六进制 MD5 值,列 remark 存用户备注(几字到几百字不等)。分别应选 char 还是 varchar?
答:
code用char(32)(长度固定、读取快、无碎片);remark用varchar(长度差异大、省空间)。与 M22 主键长度、M24 索引体积连考;建表评审高频。
【知识关联】
- 补题关联:补-24(金额用 DECIMAL,同属类型选型)、补-28(编码思想可类比:省内存 vs 灵活性)。
- 面试/工程:定长且几乎不变用 CHAR(如状态码、MD5 定长哈希),更新少、不易碎片;可变长度用 VARCHAR,更省空间但更新可能导致行迁移。UTF-8 下 VARCHAR(n) 的 n 是字符数,实际字节上限受 row_format 与整行 65535 限制约束。大文本不要塞 VARCHAR,用 TEXT 并注意是否溢出页存储。
- 面试追问:① VARCHAR(255) 与 VARCHAR(256) 存储差在哪?(单字节字符集下恰是长度前缀 1↔2 字节的临界点,5.0.3+ 语义;换 utf8mb4 则临界点提前到 varchar(63)/varchar(64),因为 64×4=256 已超 255) ② 为什么金额不能用 FLOAT?(二进制浮点无法精确表示十进制小数,会累计误差)
【拓展延伸】
- 变式问法:给出字段场景选类型(手机号 CHAR(11)、用户名 VARCHAR、价格 DECIMAL、简介 TEXT)。
- 参数/命令:
CHAR/VARCHAR/TEXT/DECIMAL(M,D);row_format=DYNAMIC;information_schema.TABLES看数据长度;SHOW CREATE TABLE审字符集与排序规则。
M27. 在 MySQL 5.6 及以上版本中,对已有大表执行 ALTER TABLE ... ADD INDEX ... 的实际情况是?
考点:Online DDL 加索引
A. 一定全程锁表,业务无法读写 B. 支持 Online DDL,多数场景不阻塞读写(仅开始与结束阶段需要短暂获取元数据锁) C. 大表根本无法加索引 D. 只能通过“建新表 + 导数 + 改名”的方式实现
答案:B
【考点】Online DDL 机制与 DDL 变更的可用性权衡。
【结论】 选 B。MySQL 5.6 起 ADD INDEX 走 Online DDL,绝大多数时间不阻塞读写,只有开始与结束两个瞬间需要短暂获取元数据锁。
【逐项辨析】
- A 一定全程锁表、业务无法读写:错。这是 MySQL 5.5 及更早的
COPY方式(拷全表期间禁写),5.6 之后已被 Online DDL 取代,“一定全程”是过时口径。 - B 支持 Online DDL、多数场景不阻塞读写,仅开始与结束阶段需短暂获取元数据锁:正确。 关键在于“短暂 MDL”这个限定,它才是线上真正的风险点。
- C 大表根本无法加索引:错。大表可以加索引,只是耗时长、资源占用高,需要配合低峰执行或外部工具。
- D 只能通过“建新表 + 导数 + 改名”实现:错。这是
pt-online-schema-change等外部工具的思路,属“可选方案”而非“唯一途径”,说“只能”过于绝对。
【知识点】 DDL 的三种执行算法与“期间能否并发 DML”的对应关系。
| 算法 | 执行方式 | 期间能否 DML | 典型场景 |
|---|---|---|---|
COPY | 建临时表、逐行拷贝数据 | 不能(阻塞写) | 5.5 时代;改列类型等重操作 |
INPLACE | 在原表上直接改,不拷全表 | 多数可以 | 5.6+ 的 ADD INDEX、部分 ADD COLUMN |
INSTANT | 只改元数据,秒级完成 | 可以 | 8.0+ 的 ADD COLUMN(加在末尾)、改默认值 |
Online DDL 的实现思路:在原表上直接构建新索引,同时把构建期间发生的 DML 变更记录到一块在线日志(row log)中,待索引构建完成后把这批增量补到新索引上,最后短暂加锁切换。整个过程真正的“锁”出现在两端 —— 开始阶段要拿 MDL 共享锁确认表结构、结束阶段要升级为排他锁完成切换;若此刻有长事务未提交,它会一直持有 MDL,把 DDL 卡在中间,进而阻塞该表上后续所有查询,这就是线上“加个索引把库搞挂”的经典事故链。
生产建议三条:一是低峰执行;二是先确认无长事务(查 information_schema.innodb_trx);三是超大表或需要变更列类型等 COPY 类操作,改用 pt-online-schema-change(建影子表 + 触发器同步增量)或 gh-ost(读 binlog 同步增量、不用触发器)。
【记忆锚点】 “Online DDL 两头锁、中间放开” —— 怕的不是建索引本身,而是“长事务把 MDL 卡住”。
【易混对比】
- Online DDL vs 外部工具:前者是原生能力(
ALGORITHM=INPLACE),适合常规加索引;后者适合超大表与COPY类变更,能限速、可暂停,但多一层复杂度。 - MDL 元数据锁 vs InnoDB 行锁:MDL 保护表结构,DDL 与 DML 靠它互斥;行锁保护数据行。DDL 被长事务卡住,卡的是 MDL。
INPLACEvsINSTANT:INPLACE仍要重建索引 / 修改数据,耗时可观;INSTANT只改元数据,秒级完成。- 换问法:若题目问“Online DDL 期间最大的风险是什么”,答案就是“开始 / 结束时需要 MDL,被长事务阻塞后会连锁阻塞查询”。
【自测】 业务低峰执行 ALTER TABLE big_t ADD INDEX idx_c(c),命令长时间不返回,同时监控显示该表上的普通 SELECT 也全部卡住。最可能的原因是什么?
答:存在未提交的长事务一直持有 MDL,DDL 在等待元数据锁,并把后续查询一起挡住。应先查
information_schema.innodb_trx找出长事务并结束它。与 M18 意向锁、M28 复制连考;DBA 岗高频。
【知识关联】
- 补题关联:补-22(DDL 期间 Buffer Pool/IO 压力)。
- 面试/工程:大表加索引用 Online DDL + 低峰窗口,或 gh-ost/pt-osc;必须观察磁盘与复制延迟。
- 面试追问:① Online DDL 的“online”指什么? ② 什么 DDL 仍会锁表?
【拓展延伸】
- 变式问法:5.5 及以前是复制表+重建(表锁),5.6+ 默认 Online ADD INDEX。
- 参数/命令:
ALTER TABLE t ADD INDEX idx(c), ALGORITHM=INPLACE, LOCK=NONE;innodb_online_alter_log_max_size;lock_wait_timeout。
M28. MySQL 主从复制中,从库的 SQL 线程的作用是?
考点:主从复制的线程模型
A. 从主库拉取 binlog B. 在主库上生成 binlog C. 读取 relay log 并在从库重放,把变更应用到从库数据 D. 执行 redo log 的刷盘
答案:C
【考点】主从复制的三个关键线程及其分工。
【结论】 选 C。从库 SQL 线程的职责是读 relay log 并在从库重放变更,把数据真正“落”到从库。
【逐项辨析】
- A 从主库拉取 binlog:错。这是从库 IO 线程的职责(连主库、收 binlog、写 relay log),不是 SQL 线程。
- B 在主库上生成 binlog:错。binlog 是主库事务提交时由 Server 层写入的,位置在主库不在从库,也不是由某个复制线程专门生成。
- C 读取 relay log 并在从库重放:正确。 SQL 线程是“执行者”,消费 IO 线程落地的中继日志。
- D 执行 redo log 的刷盘:错。redo log 刷盘是 InnoDB 引擎的行为(受
innodb_flush_log_at_trx_commit控制),与复制线程无关。
【知识点】 一条 binlog 从主库到从库要经过三个线程、两份日志。
| 线程 | 所在位置 | 职责 | 产出 |
|---|---|---|---|
| dump 线程 | 主库 | 监听 binlog,把变更推送给从库 | 网络传输 |
| IO 线程 | 从库 | 连接主库、接收 binlog、写入本地 relay log | relay log |
| SQL 线程 | 从库 | 读取 relay log,重放变更到从库数据 | 从库数据更新 |
流水线是:主库写 binlog → dump 线程推送 → 从库 IO 线程落 relay log → 从库 SQL 线程重放。注意“两份日志”:binlog 在主库、是复制源;relay log 在从库、只是 binlog 的本地暂存副本,重放完即可清理(relay_log_purge 默认开启)。配套参数还有 read_only / super_read_only(防止从库被误写)。
延迟的根也在这里:IO 线程始终是单线程,而 SQL 线程在 5.6 之前只能单线程重放;5.6 起可按库并行、5.7 起按组提交并行、8.0.27 起 replica_parallel_workers 默认 4(本机 8.0.46 实测 @@replica_parallel_workers=4,从库回放默认已多线程),「单线程重放」只对老版本或显式设为 0/1 的实例成立(slave_parallel_workers 只作用于 SQL 线程,不影响 IO 线程)—— 主库多并发写入、从库串行重放,队列越积越长,这就是主从延迟的主要成因。并行复制的演进分三代:5.6 引入基于库(schema)粒度的并行复制(slave_parallel_workers > 0,只有分属不同库的事务能并发重放,单库写入仍然串行);5.7 才引入基于组提交的逻辑时钟 LOGICAL_CLOCK(同一组提交里的事务可并发重放,不再依赖多库);8.0 进一步用 WRITESET(依赖追踪参数 binlog_transaction_dependency_tracking 自 5.7.20 提供;8.0 的默认值不是 WRITESET——本机全新 8.0.46 容器实测 @@binlog_transaction_dependency_tracking=COMMIT_ORDER(@@transaction_write_set_extraction=XXHASH64),要 WRITESET 得显式设;8.0.35/8.2.0 起废弃,8.4 起参数移除、多线程回放时源端固定按 write set 生成依赖信息(等效 WRITESET)),并行度最高。
【记忆锚点】 “主库 dump 往外推,从库 IO 往回收,从库 SQL 往里放” —— 一推、一收、一放,对应 binlog → relay log → 数据。
【易混对比】
- IO 线程 vs SQL 线程:IO 线程负责“把日志搬回来”(网络 + 落盘),SQL 线程负责“把日志执行掉”(重放)。延迟高时要先判断是 IO 落后还是 SQL 落后,优化方向完全不同。
- binlog vs relay log:前者在主库、是复制源;后者在从库、是复制中转,内容同源。
- 主库 dump 线程 vs 从库 SQL 线程:前者只发送、不执行;后者只执行、不传输。
- 换问法:若题目问“从库 IO 线程的作用”,答案就换成“连接主库拉取 binlog 并写入 relay log”(与本题互为镜像,务必成对记)。
【自测】 从库 SHOW SLAVE STATUS 显示 Slave_IO_Running: Yes、Slave_SQL_Running: Yes,但 Seconds_Behind_Master 持续增大。是哪个线程跟不上?
答:SQL 线程。IO 线程正常说明 binlog 能拉回来,延迟来自重放能力不足,优化方向是开并行复制(正名
replica_parallel_workers,8.0.26 起改名、slave_parallel_workers为废弃别名;8.0.27 起默认 4)。与 M29 连考;运维面试高频。
【知识关联】
- 补题关联:补-20(redo/undo 与检查点的崩溃恢复判据;复制链路里的日志角色见本题正文)。
- 面试/工程:单线程 SQL 线程是延迟主因之一;并行复制的标准演进是 5.6 基于库(schema)粒度 → 5.7 基于组提交的
LOGICAL_CLOCK→ 8.0 基于WRITESET,版本归属别记混。 - 面试追问:① IO 线程与 SQL 线程分别做什么? ② 并行复制如何保证事务顺序?
【拓展延伸】
- 变式问法:问“relay log 是谁写的”(IO 线程);问“谁负责重放”(SQL 线程)。
- 参数/命令:
SHOW REPLICA STATUS(8.0.22+)/SHOW SLAVE STATUS;replica_parallel_workers、binlog_transaction_dependency_tracking(取值COMMIT_ORDER/WRITESET)。
M29. 下列不属于主从延迟常见原因的是?
考点:主从延迟的原因
A. 从库 SQL 线程单线程重放,跟不上主库并发写入 B. 主库执行了长时间的大事务(如一次删除百万行) C. 从库硬件配置远低于主库 D. 主库使用了 InnoDB 存储引擎
答案:D
【考点】主从延迟的成因与缓解手段。
【结论】 选 D。InnoDB 是主从双方通用的存储引擎,它既不制造延迟、也不区分主从,与延迟无因果关系。
【逐项辨析】
- A 从库 SQL 线程重放能力跟不上主库并发写入:是真实成因。5.6 之前 SQL 线程只能单线程串行重放,即便 5.6 起有了并行复制(8.0.27 起默认 4 个 worker),并行粒度、并行度配置与主库的并发写入之间仍可能失衡,重放排队积压,历来是延迟的第一大来源。
- B 主库执行长时间大事务(如一次删除百万行):是真实成因。大事务产生的 binlog 必须整体重放完才在从库可见,期间延迟持续累积。
- C 从库硬件配置远低于主库:是真实成因。磁盘 IOPS、CPU、内存不足会直接拖慢重放速度。
- D 主库使用了 InnoDB 存储引擎:不是成因。 InnoDB 是 MySQL 默认引擎,主从两侧通常都用它,既不制造也不放大延迟,属于“与结论无关的选项”。
【知识点】 主从延迟的本质是“从库消费 binlog 的速度跟不上主库生产的速度”,一切成因都能归到这条流水线的某个环节。
| 环节 | 成因 | 表现 | 缓解手段 |
|---|---|---|---|
| 主库生产 | 大事务(一次删百万行、大批量 INSERT) | 单事务 binlog 达几百 MB,必须整体重放 | 拆小事务、分批提交 |
| 网络传输 | 带宽不足、跨机房链路抖动 | Slave_IO_Running 落后 | 提升带宽、就近部署 |
| 从库重放 | SQL 线程单线程 / 并行粒度不够 | Seconds_Behind_Master 持续增大 | 开并行复制(slave_parallel_workers) |
| 从库资源 | 硬件弱、还承担大量只读查询 | 重放被只读流量挤占 | 升配、读流量分流到专用从库 |
| 锁冲突 | 从库上长查询 / 大事务占锁 | 重放线程被阻塞 | 控制从库大查询 |
诊断思路:先看 Seconds_Behind_Master,再分别看 Slave_IO_Running 与 Slave_SQL_Running —— IO 线程落后说明“日志搬不回来”(网络 / 主库压力),SQL 线程落后说明“日志执行不完”(重放能力 / 锁冲突)。补两个口径:一是 Seconds_Behind_Master 只是估算值(基于主库 binlog 时间戳与从库当前时间之差),主库长时间无写入时它可能显示 0 却掩盖真实差距,更可靠的是 pt-heartbeat 这类心跳工具;二是主从延迟无法彻底消除,只能压到业务可接受的范围,对一致性要求高的读(如刚下单后立刻查订单)应强制走主库。
【记忆锚点】 “生产快、传输慢、重放更慢” —— 延迟由三段流水线里“最慢那一环”决定,InnoDB 不在任何一环里。
【易混对比】
- 主从延迟 vs 主从数据不一致:延迟是“暂时落后”(最终会追上),不一致是“永久错误”(误操作、复制中断、
sync_binlog配置不当导致丢事务),严重程度完全不同。 - 异步复制 vs 半同步复制(
semi-sync):异步下主库不等从库确认就返回,延迟不影响主库;半同步要等至少一个从库收到 binlog 才返回,能降低丢数据风险,代价是增加主库提交延迟。 - 延迟 vs 复制中断:
Slave_SQL_Running: No是复制停了(如主键冲突),不是“慢”,需人工介入跳过或修复。 - 换问法:若题目改成“下列属于主从延迟原因的是”,A / B / C 任选其一都对,D 仍不对 —— 否定型题目要把每个选项独立判真伪,不要被“哪个更像”带偏。
【自测】 主库一次 DELETE FROM log WHERE dt < '2025-01-01' 删掉 800 万行,此后从库 Seconds_Behind_Master 持续增长数小时。最有效的直接措施是什么?
答:把大事务拆成多批小事务(如按天分批
DELETE ... LIMIT 10000循环),让 binlog 分片、从库可持续追赶。与 M28 连考;运维与后端面试高频。
【知识关联】
- 补题关联:补-20(redo/undo 与检查点的崩溃恢复判据;复制链路里的日志角色见本题正文)、补-18(主从并发重放与 2PL/锁的关系可对照理解)。
- 面试/工程:读写分离架构必须容忍秒级延迟;关键读(支付结果、库存扣减后的确认)应强制走主库。延迟五大主因:大事务、无主键/无索引更新导致从库回放变慢、DDL、从库单线程或并行度不足、机器资源(CPU/IO/内存)不够。大促前要压测从库回放能力而不只是主库写入。
- 面试追问:① 从库延迟如何定量观测?(
Seconds_Behind_Source有局限;更准看performance_schema.replication_connection_status的 heartbeat 与位点差、或 GTID executed 集合差) ② 为什么“无主键表的更新”特别伤复制?(从库回放行变更时要全表扫定位行,把顺序回放打成随机 IO)
【拓展延伸】
- 变式问法:把“网络抖动 / 大事务 / 从库规格低 / 主库大 DDL”混在选项里辨认;或问“并行复制为何能降延迟”——把无冲突事务在从库并发重放。
- 参数/命令:
SHOW REPLICA STATUS\G(8.0.22+)看Seconds_Behind_Source、Retrieved_Gtid_Set/Executed_Gtid_Set;replica_parallel_workers、replica_parallel_type;binlog_transaction_dependency_tracking=WRITESET(该参数 5.7.20 引入;8.0 默认仍是COMMIT_ORDER(本机 8.0.46 实测),WRITESET 需显式开;8.0.35/8.2.0 废弃,8.4 起参数移除并固定按 write set 生成依赖信息)提高主库组提交后的可并行度;业务侧关键读/*+ MASTER */或路由到主库。
M30. 什么时候应该考虑分库分表?
考点:分库分表的时机
A. 表刚建好时就分,一劳永逸 B. 单表数据量过大(通常千万级以上)导致查询/写入性能显著下降,且已通过索引优化、缓存、读写分离仍无法满足时 C. 只要建了索引就不需要分库分表 D. 任何表都应该尽早分库分表
答案:B
【考点】分库分表的适用条件与代价。
【结论】 选 B。分库分表是最后手段 —— 只有当单表数据量过大、且索引优化 / 缓存 / 读写分离都已用尽仍不达标时,才值得付出分布式复杂度。
【逐项辨析】
- A 表刚建好时就分、一劳永逸:错。过早分片会凭空引入分布式事务、跨分片查询、全局 ID 等复杂度,而此时数据量根本不需要,属于“用大炮打蚊子”。
- B 单表过大导致性能显著下降,且已通过索引优化、缓存、读写分离仍无法满足:正确。 它同时给出了“数据量判据”与“前置手段已穷尽”两个条件,是唯一完整的表述。
- C 只要建了索引就不需要分库分表:错。过于绝对 —— 索引解决的是“查询定位”,解决不了单机写入热点、单表存储容量上限与 DDL 的维护代价。
- D 任何表都应该尽早分库分表:错。与 A 同病,“任何”“尽早”两个绝对化词直接排除;分片是按需引入的重架构变更。
【知识点】 分库分表的定位是“优化阶梯的最后一级”,顺序不可跳。
| 阶段 | 手段 | 解决什么问题 | 代价 |
|---|---|---|---|
| ① | SQL 与索引优化 | 慢查询、全表扫描 | 低(改代码 / 加索引) |
| ② | 加缓存(Redis) | 热点读、减轻库压力 | 中(一致性维护) |
| ③ | 读写分离 | 读扩展 | 中(主从延迟,见 M29) |
| ④ | 归档冷数据 | 单表行数下降 | 低(需业务配合) |
| ⑤ | 垂直拆分 | 按业务拆库、大字段独立成表 | 中(跨库 JOIN 变多) |
| ⑥ | 水平分库分表 | 突破单机容量与写入上限 | 高(分布式事务、跨片查询、全局 ID、扩容重分片) |
判断标准要落到指标而不是拍脑袋:单表行数、慢查询占比、写入延迟、磁盘占用、DDL 耗时。经验口径是“单表千万级开始评估”,但具体阈值取决于列宽、索引数量与访问模式 —— 一行 200 字节的表到 5000 万行可能依然健康,而一行 5 KB 的表 1000 万行就已吃力。真正的触发条件永远是“性能指标确实恶化了”,而不是某个固定行数。
水平分片引入的额外问题必须提前想清楚:一是分片键选择(决定能否避免跨片查询,选错会导致全片扫描);二是全局唯一 ID(自增主键失效,需雪花 ID / 号段模式,与 M22 主键选型呼应);三是跨片查询与分页(ORDER BY ... LIMIT 需各片各取再归并);四是分布式事务(跨片写要用 TCC / 本地消息表等);五是扩容重分片(从 4 库扩到 8 库要迁移数据,需预留平滑方案)。这些正是它必须“最后才用”的原因。
【记忆锚点】 “先优化、再缓存、后读写分离,实在不行才分库分表” —— 分片是“最后手段”,不是“最优手段”。
【易混对比】
- 垂直拆分 vs 水平拆分:垂直是“按业务 / 按列切”(用户库、订单库;大字段挪到附表),水平是“按行切”(同结构表按分片键分散到多个库表)。前者复杂度低、更常见,后者才是“分库分表”通常所指。
- 分库分表 vs 分区表:分区表是单机内由数据库管理的物理划分,应用无感知、无需改代码,但仍在同一实例,突破不了单机上限;分库分表是应用层方案,能真正扩展。
- 分表 vs 分库:分表解决单表过大;分库解决单实例连接数 / IO 上限。通常先分表,压力仍大再分库。
- 换问法:若题目问“分库分表后自增主键为什么不能用了”,答案就回到 M22 的“需改用雪花 ID / 号段模式生成全局唯一 ID”。
【自测】 某订单表 2000 万行、单行约 300 字节,按 create_time 排序的查询变慢,已加联合索引仍有慢查询,且缓存命中率低(订单查询按用户分散)。按优化阶梯,下一步最该做什么?
答:先做冷数据归档(把历史订单迁到归档表,把单表行数压下来),再评估是否值得水平分片 —— 分片是第 ⑥ 级,不该在 ④ 之前上。与 M24 深分页、M22 主键选型连考;架构岗与后端社招高频。
【知识关联】
- 补题关联:无直接补题;可与补-24(表设计)连读。分库分表是“设计无法再水平扩展时的最后手段”。
- 面试/工程:单表行数经验阈值(如千万级)不是铁律,更该看:索引树高、Buffer Pool 命中率、慢查询、主从延迟、备份窗口。优先顺序通常是:优化 SQL/索引 → 读写分离 → 缓存 → 垂直拆分 → 水平分库分表。过早分片会把简单问题复杂化(跨片 JOIN、分布式事务、扩容迁移)。
- 面试追问:① 为什么不能只看行数决定分表?(行宽、访问模式、索引大小、硬件都影响;500 万窄表可能仍轻松) ② 分片键怎么选?(按最高频查询维度,尽量让常用查询落在单分片)
【拓展延伸】
- 变式问法:问“什么时候该分库分表”选“容量/性能/可用性瓶颈且其他手段无效”;把“刚过一百万行就分”设为错误项。
- 参数/命令:
EXPLAIN、慢查询日志、information_schema.INNODB_BUFFER_PAGE命中;ShardingSphere/Sharding-JDBC;一致性哈希或范围分片;扩容用双写迁移。
M31. 防止 SQL 注入最有效的方式是?
考点:SQL 注入防护
A. 使用 PreparedStatement / 参数化查询(占位符绑定参数) B. 用字符串拼接构造 SQL C. 手动过滤掉单引号 D. 给相关列加上索引
答案:A
【考点】SQL 注入的原理与参数化查询的防护机制。
【结论】 防止 SQL 注入最有效的手段是参数化查询(PreparedStatement / 占位符绑定) —— 它把 SQL 的“结构”与“数据”彻底分离,选 A。
【逐项辨析】
- A 使用
PreparedStatement/ 参数化查询(占位符绑定参数):正确。 SQL 模板先被数据库预编译,语法结构在参数到达之前就已固定;参数只以“数据”身份填入占位符,无法改变语句结构。 - B 用字符串拼接构造 SQL:错。这正是漏洞的根源 —— 用户输入与 SQL 语法拼在同一条字符串里,输入中的
'会闭合原引号,其后内容被当作语法解析。 - C 手动过滤掉单引号:错。错在“过滤”二字 —— 字符替换属“打补丁”式防护,可用十六进制编码、注释符、宽字节注入(
%bf%27)等方式绕过,是不可靠方案。 - D 给相关列加上索引:错。索引只影响查询性能,与输入是否被当作语法解析完全无关,属答非所问。
【知识点】 SQL 注入的定义:应用程序把用户可控的输入未经分离处理就拼接进 SQL 语句,使输入被数据库当作 SQL 语法解析执行。其成立需两个条件同时满足:
| 成立条件 | 含义 | 参数化查询如何破坏它 |
|---|---|---|
| 输入进入 SQL 字符串 | 输入与语法处在同一段文本里 | 输入不进入 SQL 文本,只走参数通道 |
| 数据库再次解析该文本 | 输入被当成语法 | 模板先预编译,结构已固化 |
推导:SQL 注入的本质是“数据越权升级为语法”。若语句写成 SELECT * FROM user WHERE name = ' + 输入 + ',当输入为 ' OR '1'='1 时,整句变成 WHERE name = '' OR '1'='1',条件恒真 → 全表泄露。参数化查询走的是两段式协议:先 PREPARE 把模板编译成执行计划(此时结构已定),再 EXECUTE 把参数按值传入,数据库不会再对参数做语法解析,于是 ' OR '1'='1 只是一个“名字很怪的普通字符串”。
补充两点工程口径:① MyBatis 中 #{} 是预编译占位符(安全),${} 是字符串拼接(有注入风险);② 参数化查询覆盖不了表名、列名、ORDER BY 方向等结构位置,这些位置必须用白名单校验。
【记忆锚点】 「结构先定死,数据只当值」 —— 参数化查询的防护力来自“SQL 结构在参数到来之前就已经编译完成”。
【易混对比】
- 参数化查询 vs 转义 / 过滤:前者是结构性防护(让输入永远无法成为语法),后者是字符级修补(永远可能漏掉某种编码)。工程上应以前者为主、后者为辅。
#{}vs${}(MyBatis):#{}→?占位符 → 预编译,安全;${}→ 直接文本替换 → 拼接,危险。- SQL 注入 vs XSS:前者攻击数据库(伪造 SQL 语法),后者攻击浏览器(注入脚本)。二者同源(数据与代码未分离),但落点不同。
- 换问法:若题干改成“下列哪种做法不能防止 SQL 注入”,答案就落在“字符串拼接”“手动过滤单引号”上。
【自测】 MyBatis 中 ${} 与 #{} 哪个存在 SQL 注入风险?若确实需要动态指定排序字段,应如何防护?
答:
${}有风险(直接文本替换,可注入);#{}走预编译占位符,安全。动态排序字段无法参数化,必须对字段名做白名单校验。与 R06 的“输入不可信”思路同源;互联网 / 国企笔试高频。
【知识关联】
- 补题关联:补-10(视图可收窄可见列,作为纵深防御)。注:补-07 考 WHERE 与 HAVING 的作用阶段、补-06 考 DDL 语句分类,都与注入无关;本库补题暂无 SQL 注入专项,注入的工程口径见本题【知识点】与【自测】。
- 面试/工程:ORM 默认参数化即可挡住值注入;仍危险的是标识符注入(动态表名、列名、ORDER BY 方向)——必须白名单映射,绝不能拼用户输入。二次注入:恶意数据先“安全地”入库,后续查询拼进语句时才爆发,预编译救不了已入库的脏数据,必须入库前校验/转义。
- 面试追问:① 参数化能否防住所有注入?(不能防表名/列名/关键字位置;只能防值位) ② 二次注入是什么?(恶意数据被安全写入,之后在另一条动态 SQL 中被当成可信片段拼接)
【拓展延伸】
- 变式问法:对比“字符串拼接 vs PreparedStatement 参数绑定 vs 存储过程(不一定更安全)”;给出一段拼接 SQL 选注入点。
- 参数/命令:JDBC
useServerPrepStmts=true走服务端预编译;账号GRANT最小权限,禁止应用账号 DDL;sql_mode加固;WAF/网关做二次拦截;代码评审禁用字符串拼 SQL。
M32. READ COMMITTED(读已提交)能避免下列哪种并发问题?
考点:隔离级别能解决的问题
A. 脏读 B. 不可重复读 C. 幻读 D. 以上全部
答案:A
【考点】四种隔离级别与三类并发问题的对应关系。
【结论】 READ COMMITTED 只能避免脏读,不可重复读与幻读仍会发生 —— 选 A。
【逐项辨析】
- A 脏读:正确。 RC 只读取“已提交”的数据,读不到别的事务未提交的中间状态,因此脏读被杜绝。
- B 不可重复读:错。RC 下每次
SELECT都生成新的 ReadView,同一事务两次读之间其他事务提交的修改会被读到,值发生变化。 - C 幻读:错。RC 既不使用间隙锁、也不复用快照,两次范围查询之间的新增 / 删除行会被看到,幻读同样可能发生。
- D 以上全部:错。错在“全部”二字 —— RC 解决的是三者中最轻的脏读,把不可重复读与幻读也算进去,是夸大了它的能力。
【知识点】 三类并发问题的定义(均以“事务 A 读、事务 B 写”为背景):
| 并发问题 | 现象 | 反例 |
|---|---|---|
| 脏读 | 读到别的事务未提交的数据 | B 把余额 100 改成 200(未提交),A 读到 200;B 回滚,A 读到的是从未存在过的值 |
| 不可重复读 | 同一事务内两次读同一行,值不同 | A 先读余额 100,B 提交改成 200,A 再读变成 200 |
| 幻读 | 同一事务内两次范围查询,行数不同 | A 查“年龄 > 20 共 5 行”,B 插入 1 行并提交,A 再查变成 6 行 |
四级别与三类问题的完整对照:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 标准下可能(MySQL InnoDB 通过间隙锁基本避免) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
推导:脏读的根源是“读到了未提交的数据”,只要规定“只能读已提交版本”即可消除 —— 这正是 RC 做的事,所以 RC 是“解决脏读的最低成本级别”。而不可重复读与幻读的根源是“快照被刷新”:RC 每次查询都重建 ReadView,两次读之间别人一提交,快照就变了。要消除它们,必须让同一事务在多次读之间复用同一个快照(RR),或者加锁串行化(SERIALIZABLE)。
工程口径:RC 是 Oracle / SQL Server 的默认级别,也是不少互联网公司的实际选择 —— 因为它不加间隙锁、锁范围小、并发度更高,幻读交由业务幂等或乐观锁处理;而 MySQL InnoDB 默认是 RR。
【记忆锚点】 「RC 只挡脏读,RR 再挡不可重复读,SERIALIZABLE 全挡」 —— 级别越高,问题越少,并发越差。
【易混对比】
- 不可重复读 vs 幻读:前者是同一行的值变了(
UPDATE),后者是同一个范围的行数变了(INSERT/DELETE)。一个针对“值”,一个针对“集合”。 - RC vs RR:RC 每次读新建 ReadView(读最新已提交),RR 事务内复用第一个 ReadView(读快照)。这是两者一切差异的总根源。
- 隔离级别 vs 锁:隔离级别决定“要不要加锁、加什么锁”;MVCC 提供无锁的快照读。RR 下普通
SELECT走快照读(不加锁),SELECT ... FOR UPDATE走当前读(加间隙锁)。 - 换问法:若题干改成“能避免不可重复读的级别”,答案就变成“REPEATABLE READ 及以上”;若问“哪个级别不加间隙锁”,答案是“READ COMMITTED 及以下”。
【自测】 MySQL InnoDB 的默认隔离级别是什么?在该级别下,普通 SELECT(快照读)还会出现幻读吗?
答:默认 REPEATABLE READ;快照读下不会 —— 事务内复用同一个 ReadView,B 新插入的行不在快照里,A 看不到。但当前读(
SELECT ... FOR UPDATE/UPDATE)下,InnoDB 靠间隙锁(Gap Lock)+ Next-Key Lock 阻止范围内插入,从而基本避免幻读。与 M31 连考;大厂面试高频。
【知识关联】
- 补题关联:补-17(ACID 与隔离性)、补-18(2PL)、补-19(S/X 锁相容矩阵)——原理层的隔离能力矩阵。
- 面试/工程:不少公司把默认级别改成 RC:锁竞争更少、复制在 ROW 模式下更安全、死锁形态更可预期;幻读交给业务唯一约束/防重表兜底。银行/国企笔试常考“哪个级别解决哪个问题”的矩阵,必须背死。
- 面试追问:① RC 完全不能防幻读吗?(RC 无普通间隙锁,范围读两次结果可能变;但唯一索引等值插入冲突仍能挡住“重复键”类插入) ② 为什么互联网偏 RC?(并发更高、锁更少,且多数业务用唯一键防重即可)
【拓展延伸】
- 变式问法:矩阵填空:RR 能否避免幻读 → InnoDB RR + Next-Key 可“很大程度避免”,严格串行需 SERIALIZABLE;或给出四个级别选项问“哪个能避免不可重复读”。
- 参数/命令:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;;tx_isolation/transaction_isolation;注意 RC 下无普通 Gap Lock(外键/唯一检查除外),死锁与阻塞形态会变;压测对比 RC vs RR 的 TPS 与锁等待。