Java高级后端 · 全套面试通关手册(MySQL)

存储引擎默认 InnoDB,支持事务、MVCC、行锁;MyISAM 不支持事务,只有表锁。

一、MySQL 锁体系(重点)

1.锁粒度

(1)表锁:锁定整张表。MyISAM 默认表锁;开销小,锁粒度大,并发差。读锁共享,写锁排他。

(2)行锁(InnoDB) :锁单行记录。粒度小,并发高;开销大,容易死锁。行锁是基于索引实现! 如果不走索引,行锁退化为表锁。

(3)意向锁(Intention Lock):表级别的意向共享 IS、意向排他 IX。 作用:标记表里有行锁存在。当要加表锁时,通过意向锁快速判断表内是否存在行锁,不用遍历所有行。

意向锁之间互相兼容;意向锁和表读写锁互斥。

2.行锁分类

  • 共享锁 S(读锁):多个事务可以同时加 S 锁;不能写。select ... lock in share mode

  • 排他锁 X(写锁):只有持有锁事务可读可写;其他事务读写都阻塞。select ... for update

3.Gap Lock 间隙锁⭐高频

  • 锁定索引记录之间的间隙,防止幻读。只在 RR(可重复读隔离级别)存在。

  • 锁住的是索引间隙,不是物理记录。

4.Next-Key Lock(临键锁)

Next-Key = Record Lock 记录锁 + Gap 间隙锁,InnoDB RR 默认行锁算法,左开右闭区间。

当索引唯一且精准命中一条记录时,临键锁退化成记录锁。

5.幻读

同一事务内,多次查询,查到别的事务新插入的数据。RR 依靠 MVCC + 临键锁解决幻读。

6.死锁

多个事务互相持有对方需要的锁,循环等待。

  • 产生 4 条件:互斥、持有并等待、不可剥夺、循环等待。

  • InnoDB 自动检测死锁,回滚代价小的事务。

  • 避免方案:统一资源访问顺序;减少事务粒度;避免 for update 非索引字段。

二、事务隔离级别 & MVCC(顺带回顾)

1.读未提交 RU;

  1. 读已提交 RC;

  2. 可重复读 RR(MySQL 默认);

  3. 串行化。

  • RC:无间隙锁;每次 select 读取最新快照。存在幻读。

  • RR:MVCC + Next-key 锁,解决幻读。 MVCC 核心:版本链 + Read View。每行记录多个版本 undo log,事务根据 ReadView 选择可见版本。

三、SQL 调优(高频考点)

(1)Explain 执行计划关键字段

  • id:执行顺序

  • select_type:SIMPLE 简单查询,SUBQUERY 子查询

  • type(最重要)性能从好到坏:system>const>eq_ref>ref>range>index>ALL

目标至少 range,尽量 ref;ALL 是全表扫描,必须优化

  • key:实际使用索引;key_len:索引占用字节

  • rows:预估扫描行数

  • Extra: Using filesort:文件排序,没用到索引排序,需要优化 Using temporary:临时表,常见 group by 无索引 Using index:覆盖索引,性能很好

(2)索引失效场景(背诵)

  1. 索引列做运算、函数、隐式类型转换

  2. 最左前缀原则破坏(联合索引跳过左边字段)

  3. like '% xxx' 左模糊

  4. or 连接,一侧无索引

  5. MySQL 优化器判断全表扫描比索引更快,主动放弃索引

(3)索引设计原则

  1. 联合索引遵循最左前缀匹配

  2. 区分度高字段放前面

  3. 尽量覆盖索引,避免回表(回表:主键索引找到主键,再去主键树查询完整数据)

  4. 索引不宜过多,索引提升查询,降低写入性能(写需要维护索引 B + 树)

  5. 字符串索引可以前缀索引

InnoDB 主键索引是聚簇索引,数据存在叶子节点;二级索引叶子存放主键。

四、数据库参数 & 业务层面调优

  1. buffer_pool:InnoDB 核心缓存,缓存页数据,热点数据放内存。生产设服务器内存 50%~70%。

  2. redo log:崩溃恢复,事务预写日志。redo log buffer 刷盘策略 innodb_flush_log_at_trx_commit。 1:每次事务提交刷盘(默认,安全);0 每秒刷;2 提交写到 os buffer。

  3. undo log:版本链,实现 MVCC,存放旧数据版本。

  4. 慢查询:slow_query_log,long_query_time,抓取慢 SQL 优化。

五、高频深挖面试题

Q1:行锁什么时候降级表锁?

where 条件没有索引,InnoDB 无法定位行,行锁退化成表锁。

Q2:RC 为什么没有间隙锁?

RC 隔离级别不需要防止幻读,关闭 gap 锁,只使用记录锁,并发更高。

Q3:覆盖索引是什么?

查询字段全部在索引里面,不需要回表。Extra 出现 Using index。

Q4:幻读、不可重复读区别?

不可重复读:同一事务,其他事务修改 / 删除已有数据。 幻读:其他事务插入新记录,读到新行。

Q5:redo log 和 undo log 区别?

redo log:保证崩溃恢复,物理日志,持久化。 undo log:逻辑日志,保存旧版本,实现 MVCC、事务回滚。

一句话背诵总结

InnoDB 行锁基于索引,无索引退表锁;RR 隔离级别默认 Next-Key 临键锁(记录锁 + 间隙锁)

解决幻读;意向锁用于快速判断表内是否存在行锁;Explain 看 type 和 Extra,规避索引失效;MVCC 依靠 undo 版本链 + ReadView;调优优先优化 SQL 索引,合理设置 buffer_pool 与 redo 刷盘参数。

六、MySQL 事务 & MVCC

InnoDB 引擎支持事务,事务四大特性 ACID;MVCC 是 InnoDB 实现隔离级别的核心机制。

一、事务 ACID

  1. A 原子性 Atomicity:事务是最小单元,要么全部成功,要么全部失败。依靠 undo log 实现。

  2. C 一致性 Consistency:事务执行前后,数据完整性不变(约束、业务规则不变),是最终目标。

  3. I 隔离性 Isolation:多个并发事务之间相互隔离。依靠锁 + MVCC 实现。

  4. D 持久性 Durability:事务提交后,修改永久保存,宕机不丢失。依靠 redo log 实现。

二、4 种事务隔离级别(由低到高)

  1. 读未提交 RU:能读到其他事务未提交数据。问题:脏读。几乎不用。

  2. 读已提交 RC:只能读到别人已经提交的数据。解决脏读;存在不可重复读、幻读。

  3. 可重复读 RR(MySQL InnoDB 默认) :同一个事务内,多次读取同一数据,结果一致。解决脏读、不可重复读;依靠 MVCC + 临键锁解决幻读。

  4. 串行化 Serializable:最高级别,读加共享锁,写加排他锁。完全无并发问题,性能极差。

三大读问题

  • 脏读 :读到其他事务未提交的数据。RU 才有。

  • 不可重复读 :同一事务,两次读同一行,中间别的事务修改并提交,两次结果不一样。侧重修改、删除。

  • 幻读 :同一事务,多次范围查询,别的事务插入新数据,读到新行。侧重新增记录。

区分口诀:不可重复读是行数据被改 ;幻读是多出来新行。

三、MVCC 多版本并发控制⭐核心考点

MVCC 全称 Multi-Version Concurrency Control,不加锁实现读,提升并发。

InnoDB 的普通快照读(select)走 MVCC;当前读(select ... for update /lock in share mode)走锁机制。

(1)核心组件

1.undo log 回滚日志

记录数据修改前的旧版本,形成版本链。一条记录多次修改,多个版本串联成链表。 undo log 用来回滚事务 + 构建版本链实现 MVCC。

2.Read View 读视图

事务快照,决定当前事务能看到哪些版本数据。

ReadView 核心 4 个字段:

  • m_id:当前事务 ID

  • m_low_limit_id:下一个未分配事务 id

  • m_up_limit_id:活跃事务最小 id

  • m_ids:创建视图时,所有活跃未提交事务 ID 集合

可见性判断规则(背诵):

  1. 版本事务 ID < m_up_limit_id → 可见

  2. 版本事务 ID >= m_low_limit_id → 不可见

  3. 事务 ID 在 m_ids 集合内 → 不可见

  4. 其他情况,跳到版本链上一条旧版本继续判断

(2)RC 和 RR 下 ReadView 创建时机

  • RC(读已提交) :每次 select 都会生成新 ReadView。每次查询都拿最新快照,可以看到别的事务已提交修改。存在不可重复读。

  • RR(可重复读) :事务中第一次 select 才创建 ReadView,整个事务复用这一个视图。整个事务快照不变,保证可重复读。

MySQL RR 级别,MVCC 只能避免快照读的幻读;当前读的幻读需要 Next-Key 临键锁解决。

四、快照读 vs 当前读(必考区分)

(1)快照读(普通 select):读取快照版本,不加锁,走 MVCC。

(2)当前读 :读取最新数据,加行锁。语句: select ... for update、select ... lock in share mode、update、delete、insert。

五、高频深挖面试题

Q1:MVCC 怎么实现可重复读?

RR 事务第一次 select 生成 ReadView,后续查询复用这个 ReadView,版本链数据不变,保证多次读取结果一致。

Q2:redo log 和 undo log 区别

redo log:保证持久性,崩溃恢复,物理日志;事务提交刷盘。 undo log:保证原子性、MVCC,保存旧数据版本,逻辑日志。

Q3:MVCC 有没有锁?

快照读无锁;当前读依旧要加行锁。MVCC 不是替代锁,二者配合。

Q4:RR 可以完全杜绝幻读吗?

快照读:MVCC 避免幻读; 当前读:依靠 Next-key 临键锁锁住间隙,防止其他事务插入新数据,杜绝幻读。二者配合。

Q5:为什么 RC 没有间隙锁?

RC 隔离级别不需要解决幻读,关闭 Gap 锁,只有记录锁,并发更高。

一句话背诵总结

事务 ACID:undo 保证原子,redo 保证持久,MVCC + 锁实现隔离;4 隔离级别 RU/RC/RR/ 串行;脏读、不可重复读、幻读;

MVCC 依靠 undo 版本链 + ReadView;RR 首次 select 生成 ReadView,RC 每次 select 新建;快照读走 MVCC 不加锁,当前读加行锁;RR 通过 MVCC + 临键锁解决幻读。

七、MySQL B + 树索引原理

InnoDB 索引底层采用 B + 树,聚簇索引设计,是面试核心。B 树、B + 树是多路平衡查找树,不是二叉树。

一、B + 树特点(对比 B 树)

  1. 所有数据都存叶子节点;非叶子节点只存索引键 + 子节点指针,不存完整数据。

  2. 叶子节点通过双向链表串联,有序,范围查询极强。

  3. 所有查找,最终都会落到叶子节点。

  4. 非叶子节点仅做索引导航,树高度低,IO 次数少。

B 树:每个节点都存 key + 数据;范围查询差。

二、聚簇索引 & 二级索引⭐高频

(1)聚簇索引(主键索引)

  • InnoDB主键就是聚簇索引。整张表数据直接放在主键 B + 树叶子节点。

  • 叶子节点:主键 + 完整行数据。

一张表只能有一个聚簇索引。没有主键,InnoDB 选唯一非空索引;都没有自动生成隐藏 rowid 作为聚簇索引。

(2)二级索引(普通索引)

  • 叶子节点:索引列的值 + 主键值 ,不存储整行数据。

  • 查询流程:先在二级索引找到主键,再拿主键去聚簇索引查询完整数据,这个过程叫回表。

覆盖索引:查询字段全部在二级索引叶子节点,不需要回表。Explain 出现 Using index。

三、索引页结构

B + 树节点默认一页 16KB。一页可以存放大量索引 key,所以 B + 树高度很低。 百万级数据,B + 树高度一般 2~3 层。一次磁盘 IO 读取一页,查询最多 2~3 次磁盘 IO。

四、最左前缀原则(联合索引)

联合索引:index(a,b,c),索引按 a→b→c 排序。

规则:从左向右匹配,遇到范围查询(> < between like)就停止匹配。

  • 有效:where a=?;where a=? and b=?;where a=? and b=? and c=?

  • 失效:where b=?(跳过最左 a);where a>? and b=?,b 后面字段失效。

等值放前面,范围放最后。

五、索引分类补充

  1. 主键索引:唯一,非空,聚簇索引。

  2. 唯一索引:索引值唯一,可以 null。

  3. 普通索引:无唯一性约束。

  4. 联合索引:多个字段组合索引。

  5. 前缀索引:长字符串,取前 N 个字符建立索引,节省空间。

六、高频深挖面试题

Q1:为什么不用二叉搜索树 / 红黑树做数据库索引?

二叉树深度高,大数据量下磁盘 IO 次数太多;红黑树是二叉树,节点少,深度依然远高于 B + 树。B + 树一页存大量 key,树矮,IO 少。

Q2:为什么 B + 树叶子节点用双向链表?

范围查询、排序直接遍历链表,不需要中序遍历,性能高;分页查询友好。

Q3:为什么二级索引存主键而不是物理地址?

如果存物理地址,主键更新、页分裂会导致大量索引维护。主键稳定,聚簇索引页移动不影响二级索引。

Q4:页分裂、页合并?

页满,插入新数据会分裂成新页,叫页分裂 ,影响性能; 删除数据,页面空闲空间太多,触发页合并。

减少页分裂:主键尽量自增;随机主键(uuid)容易频繁页分裂。

Q5:为什么推荐自增主键?

自增主键有序,写入在 B + 树末尾追加,几乎不会触发页分裂;UUID 无序,随机插入,频繁页分裂,IO 压力大。

一句话背诵总结

InnoDB 索引底层 B + 树;非叶子节点只存索引 key,叶子节点存放数据;聚簇索引叶子是完整行,二级索引叶子存主键,查询需要回表;联合索引遵守最左前缀;自增主键减少页分裂,B + 树链表优化范围查询。

八、MySQL redo log、undo log、binlog

三大日志是 InnoDB 事务、崩溃恢复、主从复制核心,高频对比提问。

一、redo log(重做日志)

  1. 作用 :保证事务持久性,实现崩溃恢复。

落盘思路:WAL 预写日志,先写日志,再刷数据页。

  1. 写入流程:事务修改数据页,先写 redo log buffer;事务提交时,redo 写入 os cache,根据innodb_flush_log_at_trx_commit决定是否刷磁盘。数据库宕机,利用 redo log 把已经提交事务的数据恢复到磁盘。

  2. 性质:物理日志,记录 "某个数据页被改成什么内容"。

  3. 文件:固定大小环形文件组(ib_logfile0、ib_logfile1),循环复用。

innodb_flush_log_at_trx_commit三值:

  • 1(默认):事务提交,redo log 刷磁盘。最安全,性能略低。

  • 0:每秒刷盘,事务提交不刷。宕机丢 1s 数据。

  • 2:事务提交写入系统缓存,每秒刷盘。宕机可能丢系统缓存内数据。

二、undo log(回滚日志)

  1. 作用 :①事务原子性,失败时回滚;②构建版本链,支撑 MVCC 实现快照读。

  2. 性质:逻辑日志,记录数据修改前旧版本。不是物理页还原,是反向操作(insert 对应 delete,update 反向 update)。

  3. 生命周期:事务提交不会立刻删除 undo;MVCC 还有事务需要读取旧版本时,undo 保留;没有事务依赖后,后台 purge 线程清理。

redo 是存修改后的数据;undo 存修改前旧数据。

三、binlog(二进制日志)

  1. 作用 :MySQL 服务层面日志,用于主从复制、数据备份恢复。不属于 InnoDB,是 Server 层日志。

  2. 性质:逻辑日志,记录 SQL 语句 / 行变更,记录执行成功后的事件。

  3. 三种格式:

  • statement:记录原始 SQL,日志体积小;函数、随机函数可能主从不一致。

  • row(生产推荐):记录行数据变更,不依赖 SQL,主从数据一致;日志量大。

  • mixed:混合模式,自动选择 statement/row。

  1. 写入时机:事务提交阶段写入 binlog cache,提交后刷入 binlog 文件。文件不断追加,不会循环覆盖。

四、两阶段提交(2PC,必考!redo + binlog)

目的:保证 redo log 和 binlog 数据一致,防止主从数据不一致。

  1. Prepare 阶段:写 redo log,状态标记 prepare;binlog 写入 cache,不提交。

  2. Commit 阶段:binlog 持久化磁盘;redo log 标记 commit。 崩溃恢复判断:

  • redo prepare,并且 binlog 完整:commit 事务

  • redo prepare,binlog 缺失:回滚事务

参数sync_binlog:sync_binlog=1,每次事务提交 binlog 刷盘,保证主从安全。 生产组合:innodb_flush_log_at_trx_commit=1,sync_binlog=1,ACID 最强,性能损耗大。

五、三大日志对比速记

  1. redo log:InnoDB 引擎、物理日志、崩溃恢复、环形复用、WAL。

  2. undo log:InnoDB 引擎、逻辑日志、回滚 + MVCC。

  3. binlog:MySQL Server 层、逻辑日志、主从复制 + 备份、追加写入。

高频深挖面试题

Q1:redo 和 binlog 区别?

redo 属于 InnoDB,物理日志,崩溃恢复,循环写;binlog 是 server 层,逻辑日志,主从备份,追加写。2PC 保证两者一致。

Q2:为什么需要 2PC?

如果先写 redo 再写 binlog:redo 成功,binlog 写失败。崩溃恢复后事务提交,但是从库没有 binlog,主从不一致。 反过来先 binlog 后 redo:binlog 成功,redo 崩溃没写入。主库回滚,从库执行 binlog,主从不一致。两阶段提交解决这个问题。

Q3:binlog 和 undo log 区别?

binlog 记录修改后的事件,用于备份主从;undo 记录修改前旧版本,用于回滚和 MVCC。

Q4:redo log buffer 什么时候刷盘?

事务提交、buffer 满、后台线程定时刷盘。

Q5:如果宕机发生在 prepare 之后,commit 之前,会怎么样?

MySQL 崩溃恢复时检查 binlog 是否完整。binlog 完整则提交;binlog 不完整则回滚。

一句话背诵总结

redo log 是 InnoDB 物理日志,WAL 实现崩溃恢复;

undo log 保存旧版本,支持事务回滚和 MVCC;

binlog 是 server 层逻辑日志,用于主从复制和备份;

两阶段提交 2PC 保证 redo 和 binlog 一致性,保障主从数据统一。

九、MySQL 主从复制

核心作用:读写分离、数据备份、故障切换;默认异步复制,存在数据不一致风险。

一、主从复制基础架构

  • Master 主库:负责写,同时记录 binlog。

  • Slave 从库:拉取主库 binlog,回放执行,保持数据同步。 从库包含 3 个核心线程:

  1. IO 线程:和主库建立连接,读取主库 binlog,写入本地 relay log(中继日志)。

  2. SQL 线程:读取本地 relay log,解析并执行日志里的 SQL,落地数据。

MySQL8.0 后,SQL 单线程瓶颈优化为并行复制。

二、完整复制流程

  1. Master 开启 binlog,所有 DML/DDL 操作写入 binlog。

  2. Slave 的 IO 线程连接 Master,请求指定位置之后的 binlog。

  3. Master 启动 dump 线程,推送 binlog 事件给 Slave IO 线程。

  4. IO 线程接收 binlog,写入本机 relay log 中继日志。

  5. Slave 的 SQL 线程读取 relay log,解析执行,同步数据。

  6. 从库记录已经同步到的 binlog 位置,下次断点续传。

三、复制模式(3 种)⭐高频

1.异步复制(默认)

主库写完 binlog 直接返回客户端,不等待从库同步。

优点:性能高;

缺点:主库宕机,binlog 还没传到从库,数据丢失。

2.半同步复制(semi-sync)

主库提交后,等待至少一台从库接收 binlog 并 ack 确认,再返回客户端成功。

不是等从库执行完,只是等从库收到日志。

优点:降低丢数据风险;

缺点:有等待延迟,性能下降。 两个超时分支:超时自动降级为异步。

3.并行复制

解决老版本 SQL 单线程回放 relay log,主从延迟大的问题。

原理:同一事务组的事务,可以并行回放。MySQL8.0 增强。

四、binlog 三种格式回顾(主从重点)

  1. statement:记录 SQL 语句,日志小;函数、rand () 等场景主从数据不一致。

  2. row(生产推荐):记录行变更,不依赖 SQL 上下文,主从一致性强;日志体积更大。

  3. mixed:混合模式,自动选择 statement/row。

五、主从延迟(面试高频)

产生原因

  1. 主库写入并发高,从库 SQL 线程回放速度跟不上;旧版本单线程回放。

  2. 从库硬件弱、索引缺失,大事务(大批量 update)。

  3. 网络延迟,大 binlog 传输慢。

  4. 主库大事务,binlog 一次性推送,从库长时间执行。

解决方案

  1. 开启并行复制,提升从库回放能力

  2. 优化大事务,拆分事务,避免一次性生成超大 binlog

  3. 主从硬件匹配,从库建好索引

  4. 选用 row 格式 binlog;网络优化

  5. 业务规避:不要在从库跑大量复杂查询,抢占资源

六、主从数据不一致场景

  1. 主从复制延迟,查询从库读到旧数据

  2. 异步复制,主宕机切换,部分 binlog 未同步

  3. 从库人为写入数据(禁止从库写)

  4. binlog 格式问题,statement 模式下非确定性函数

七、GTID(全局事务 ID)⭐重点

GTID:Global Transaction ID,全局唯一事务编号,uuid:事务号。

作用:

  1. 主从切换时,不用手动找 binlog 文件名 + position,自动定位同步位点。

  2. 保证同一个事务只在从库执行一次,防止重复回放。

故障切换场景,GTID 极大简化运维。

高频深挖面试题

Q1:relaylog 是什么?

中继日志,从库 IO 线程拿到主库 binlog,先存 relaylog;SQL 线程读取 relaylog 执行。防止 IO 和 SQL 线程耦合。

Q2:半同步复制,主库等待从库什么 ack?

等待从库接收 binlog 写入 relaylog 成功,不是等 SQL 线程执行完成。

Q3:读写分离有什么坑?

主从延迟,写之后立刻查从库,读不到最新数据。方案:强一致性查询直接查主库。

Q4:主从复制是同步 redo 还是 binlog?

同步 binlog。主从复制是 MySQL Server 层机制,基于 binlog,和 InnoDB redo log 无关。

Q5:主库宕机,怎么选新主?

优先选择已经同步最多 binlog的从库,减少数据丢失。

一句话背诵总结

主从复制依靠 binlog;主库 dump 线程推送 binlog,从库 IO 线程写入 relaylog,SQL 线程回放;分为异步、半同步、并行复制;异步可能丢数据;

主从延迟多由大事务、单线程回放导致;

GTID 提供全局事务 ID,简化主从切换,自动定位同步位点。

十、MySQL 慢查询优化实战

面试重点:如何发现慢 SQL → 定位原因 → 优化手段,结合 Explain 执行计划。

一、慢查询基础

(1)慢查询日志 slow_query_log 开启后,执行时间超过阈值long_query_time的 SQL 会记录到日志。

  • long_query_time:默认 10 秒,生产一般设置 0.5~1 秒。

  • log_queries_not_using_indexes:记录没有使用索引的 SQL,排查漏建索引。

注意:慢日志会有少量性能损耗,线上可按需开启,或用 pt-query-digest 分析。

(2)其他抓慢 SQL 手段

  • show processlist:实时查看当前正在执行 SQL,看长时间 running 的会话。

  • performance_schema:数据库内置性能监控,采集 SQL 执行信息。

  • 第三方工具:pt-query-digest(percona 工具集),聚合分析慢日志。

二、Explain 执行计划分析(核心)

执行explain + SQL,重点看这几个字段:

(1)type(访问类型,优先级最高) 性能排序:system > const > eq_ref > ref > range > index > ALL

  • ref:普通索引等值查询,推荐

  • range:范围查询(> < between in),可接受

  • ALL:全表扫描,必须优化

(2)key:实际用到的索引,NULL 代表没走索引。

(3)rows:预估扫描行数,数值越大越慢。目标尽量让 rows 越小。

(4)Extra额外信息高频标识

  • Using index:覆盖索引,优秀,无需回表

  • Using filesort:文件排序,严重问题,无法利用索引排序,需要额外内存 / 磁盘排序

  • Using temporary:创建临时表,常见 group by,性能差

三、慢 SQL 常见原因(背诵)

  1. 没建索引,触发全表扫描 ALL

  2. 索引失效:函数运算、隐式转换、like 左模糊、or、破坏最左前缀

  3. 大事务:单次事务处理海量数据,锁等待、binlog 暴涨,主从延迟

  4. 索引过多:写入(insert/update/delete)维护索引开销大

  5. 查询返回大量数据:select *,一次性查出几十万行,网络 + 内存压力

  6. join 多表关联,关联字段无索引

  7. order by、group by 字段无索引,触发 Using filesort / Using temporary

  8. 锁等待:行锁被其他事务持有,SQL 阻塞,执行时间拉长

四、优化方案(实战方案)

(1)索引优化

  • 禁止 select *,只查需要字段,尽量走覆盖索引,避免回表

  • 联合索引遵循最左前缀;等值条件放前面,范围放后面

  • 避免索引列做函数、计算,where substr(name,1,3)='abc'不走索引

  • 避免隐式类型转换:where id='123',id 是数字,字符串常量触发转换,索引失效

  • like 只允许xxx%,禁止%xxx左模糊。必须模糊检索可用全文索引。

(2)SQL 写法优化

1.分页优化:limit offset,size,offset 很大时越查越慢

sql 复制代码
-- 低效 
select * from table limit 100000,20; 

-- 优化:主键过滤 
select * from table where id>100000 limit 20;

2.in 列表中的值不宜过多(建议控制在 1000 以内);当集合较大时,尽量将 in 改写为 join。

3.尽量避免使用 not in、!= 等写法,否则会导致索引失效。

4.减少子查询的使用,优先采用 join 关联;join 的关联字段必须建立索引,并遵循小表驱动大表的原则。

5.拆分大 SQL,避免一次性查询海量数据。

(3)业务层面优化

  1. 大事务拆小,不要一次更新几万条数据,减少锁持有时间,降低主从延迟

  2. 冷热数据分离,历史数据归档,单表数据量过大做分库分表

  3. 读多写少场景,引入 Redis 缓存,减轻 DB 压力

  4. 读写分离,读请求走从库;注意主从延迟问题

(4)参数层面优化

  1. innodb_buffer_pool_size:缓存热点数据页,服务器内存 50%~70%

  2. sort_buffer_size、join_buffer_size:单会话缓冲区,不要调太大,容易内存暴涨

  3. innodb_flush_log_at_trx_commit,根据业务安全要求权衡性能

五、锁等待类慢 SQL 排查思路

SQL 本身执行很快,但是等待锁,耗时很长:

  1. show engine innodb status; 查看事务、锁等待信息

  2. 找到持有锁的长事务,kill 会话

  3. 缩短事务执行时间,事务内不要放外部接口调用

高频深挖面试题

Q1:limit 大偏移量为什么慢?

数据库要扫描并丢弃前面 offset 条记录,才返回后面数据。用主键 ID 过滤优化。

Q2:Using filesort 一定是磁盘排序吗?

不一定。优先在内存排序,内存不够才落地磁盘;无论内存还是磁盘,都代表没有索引排序,需要优化。

Q3:为什么不建议 select * ?

1.读取多余字段,IO 增加;

  1. 无法触发覆盖索引,发生回表;

  2. 网络传输量大。

Q4:索引建的越多越好么?

不是。查询变快,写入变慢。每次 insert/update/delete 都要维护 B + 树索引,索引越多写入压力越大。

Q5:大 in 怎么优化?

in 数量少直接用;数量很大,拆分成批量查询,或者改成 inner join。

一句话背诵总结

慢查询通过慢日志、processlist 捕获;

Explain 重点看 type、rows、Extra;慢 SQL

根源:缺少索引、索引失效、大事务、大分页、join 无索引;

优化优先改写 SQL + 合理建索引,避免 select *,利用覆盖索引;

超大表冷热分离,引入缓存分担压力;锁等待类慢 SQL,缩短事务时长。

十一、分库分表原理与痛点

适用场景:单表数据量千万级以上,查询、写入性能下降;

单库 CPU/IO 达到瓶颈。核心分为垂直拆分、水平拆分。

一、两种拆分方式

(1)垂直拆分(纵向)

  • 分库:按业务模块拆库。例如订单库、用户库、商品库,把不同业务表拆分到不同数据库实例。

  • 分表:一张大表,按字段拆成多张表。如 user 表拆 user 基础表(id,name,phone)、user_ext 扩展表(id,remark,avatar)。

特点:表结构不一样;解决单表字段过多、大字段(text)拖慢查询问题。

缺点:跨表 join 变成跨库 join,性能差。

(2)水平拆分(横向,面试重点)

表结构不变,按数据行拆分,分到多个库 / 多张表。 常见分片策略:

1.取模分片 :shard = id % N。id 对分表数量取模,路由到对应表。

✅优点:数据分布均匀;

❌缺点:扩容困难,扩容需要迁移大量数据。

2.范围分片:按 id 区间、时间区间拆分。如 0~100w 在 t_order_0,100w~200w 在 t_order_1。

✅优点:扩容简单;

❌缺点:热点问题,新数据全部落在最后一张表。

3.一致性哈希:对分片 key 做 hash 映射到哈希环;新增节点只迁移部分数据。

分片 key 选择:一般选业务主键,如 orderId;尽量避免跨分片查询。

二、中间件分类(了解)

1.客户端分片:Sharding-JDBC。jar 包形式,应用层做 SQL 解析路由,无独立代理。

2.服务端代理分片:MyCat。独立中间件,数据库代理,应用连接 mycat,由代理路由。

三、核心难点 & 痛点⭐高频考点

(1)跨分片 Join 问题

拆分后数据分布在不同库,原生无法直接 join。 解决方案:

  • 业务层 Join:先查 A 分片,拿到 ID 集合,批量查 B 分片,内存组装。

  • 冗余字段:宽表,提前冗余关联字段,消除 join。

  • 全局表:字典表(地区、分类)在所有分片都保存一份。

(2)分布式 ID(必考)

分表后,不能依赖数据库自增 ID(多个表自增会重复)。 方案:

  1. 雪花算法 Snowflake:64bit,时间戳 + 机器号 + 序列号;本地生成,高性能。缺点依赖系统时钟,时钟回拨会重复。

  2. 数据库号段模式:预分配一段 ID,用完再取下一段。

  3. Redis 自增生成 ID。

(3)分布式事务

跨库操作,本地事务失效。 方案:

  • 最终一致性:TCC、SAGA、本地消息表、事务消息(RocketMQ)

  • 强一致性:XA 2PC,性能差,生产极少用。

业务优先:尽量设计成单分片事务,规避分布式事务。

(4)分页、排序、聚合

order by / limit跨分片:每个分片单独查询,应用内存汇总再排序分页。 大 limit 分页性能极差。 优化:

  • 带上分片 key 查询;

  • 避免深度分页;

  • 时间 / ID 条件过滤,减少汇总数据。

(5)扩容迁移问题

取模分片最大痛点:节点增加,取模结果变化,大量数据需要搬迁。 方案:

  • 双写迁移:旧分片继续写入,同步迁移数据,数据对齐后切换路由,灰度上线。

  • 预分片,预留足够分片数。

(6)全局唯一约束

单库唯一索引失效。例如手机号唯一,数据分布多个分片,数据库无法全局校验。

解决:通过 Redis / 分布式锁做唯一性校验。

四、分库分表落地原则(背诵)

  1. 能不分就不分。优先索引优化、读写分离、冷热归档。千万以下不轻易拆分。

  2. 分片 key 慎重选择,尽量让同一业务数据落在同一个分片,减少跨分片查询。

  3. 避免跨库事务、跨库 join。

  4. 预估未来数据量,提前规划分片数量,减少后续扩容成本。

高频深挖面试题

Q1:垂直拆分和水平拆分区别?

垂直:按业务 / 字段拆分,表结构不同;水平:按行拆分,表结构相同。

Q2:雪花算法时钟回拨怎么解决?

记录上一次生成 ID 的时间戳;如果当前时间小于上次时间,等待时钟追上,或抛出异常。

Q3:分片 key 为什么很重要?

查询不带分片 key,会触发全分片广播查询,性能暴跌。

Q4:分库分表和读写分离区别?

读写分离:同一套数据,主写从读,解决读压力。 分库分表:数据切割到多个库,解决单库容量、读写瓶颈。两者可以一起使用。

Q5:什么场景不适合分库分表?

查询条件不固定,大量不带分片 key 的查询;大量跨分片 join;业务简单,数据量不大。

一句话背诵总结

分库分表分为垂直(业务 / 字段)、水平(数据行)拆分;

分片策略有取模、范围、一致性 hash;

痛点集中在跨分片 join、分布式 ID、分布式事务、分页排序、数据扩容迁移、全局唯一约束;

落地原则优先优化索引、读写分离,尽量避免跨分片操作,能不分则不分。

相关推荐
光依旧1 小时前
MCP实战手记(八):从“能跑“到“能上线“——无状态MCP Server的生产落地清单
java·人工智能·spring boot·架构·ai agent·mcp
java1234_小锋2 小时前
【技术专题】Mysql8 数据库 - Mysql8 简介 & 安装以及配置
数据库·mysql
弹简特2 小时前
【Java项目-企悦抽】15-活动管理模块02-活动创建测试与活动列表实现
java·开发语言·springboot
斯内普吖2 小时前
(开源)水果蔬菜商城实战指南 基于 Java + SSM + Vue + MySQL
java·vue.js·mysql·开源
wang_shu_mo_ran2 小时前
Spring IoC和DI概念篇
java·后端·spring
小狼154542 小时前
拼多多订单数据导出 CSV 实战:接口分页、字段平铺与 5 个数据坑,多多开票助手
java·前端·javascript
Joe_Wang53 小时前
【从0到1学习JVM · 25】同样都要停顿所有线程,Parallel比Serial到底强在哪
java·jvm·学习·垃圾回收
wuminyu3 小时前
JVM虚拟线程的底层实现原理分析
java·linux·c语言·jvm·c++
做运维的阿瑞3 小时前
mysql数据库分组查询:GROUP BY 与 HAVING 的执行逻辑
数据库·sql·mysql