存储引擎默认 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;
-
读已提交 RC;
-
可重复读 RR(MySQL 默认);
-
串行化。
-
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)索引失效场景(背诵)
-
索引列做运算、函数、隐式类型转换
-
最左前缀原则破坏(联合索引跳过左边字段)
-
like '% xxx' 左模糊
-
or 连接,一侧无索引
-
MySQL 优化器判断全表扫描比索引更快,主动放弃索引
(3)索引设计原则
-
联合索引遵循最左前缀匹配
-
区分度高字段放前面
-
尽量覆盖索引,避免回表(回表:主键索引找到主键,再去主键树查询完整数据)
-
索引不宜过多,索引提升查询,降低写入性能(写需要维护索引 B + 树)
-
字符串索引可以前缀索引
InnoDB 主键索引是聚簇索引,数据存在叶子节点;二级索引叶子存放主键。
四、数据库参数 & 业务层面调优
-
buffer_pool:InnoDB 核心缓存,缓存页数据,热点数据放内存。生产设服务器内存 50%~70%。
-
redo log:崩溃恢复,事务预写日志。redo log buffer 刷盘策略 innodb_flush_log_at_trx_commit。 1:每次事务提交刷盘(默认,安全);0 每秒刷;2 提交写到 os buffer。
-
undo log:版本链,实现 MVCC,存放旧数据版本。
-
慢查询: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
-
A 原子性 Atomicity:事务是最小单元,要么全部成功,要么全部失败。依靠 undo log 实现。
-
C 一致性 Consistency:事务执行前后,数据完整性不变(约束、业务规则不变),是最终目标。
-
I 隔离性 Isolation:多个并发事务之间相互隔离。依靠锁 + MVCC 实现。
-
D 持久性 Durability:事务提交后,修改永久保存,宕机不丢失。依靠 redo log 实现。
二、4 种事务隔离级别(由低到高)
-
读未提交 RU:能读到其他事务未提交数据。问题:脏读。几乎不用。
-
读已提交 RC:只能读到别人已经提交的数据。解决脏读;存在不可重复读、幻读。
-
可重复读 RR(MySQL InnoDB 默认) :同一个事务内,多次读取同一数据,结果一致。解决脏读、不可重复读;依靠 MVCC + 临键锁解决幻读。
-
串行化 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 集合
可见性判断规则(背诵):
-
版本事务 ID < m_up_limit_id → 可见
-
版本事务 ID >= m_low_limit_id → 不可见
-
事务 ID 在 m_ids 集合内 → 不可见
-
其他情况,跳到版本链上一条旧版本继续判断
(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 树)
-
所有数据都存叶子节点;非叶子节点只存索引键 + 子节点指针,不存完整数据。
-
叶子节点通过双向链表串联,有序,范围查询极强。
-
所有查找,最终都会落到叶子节点。
-
非叶子节点仅做索引导航,树高度低,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 后面字段失效。
等值放前面,范围放最后。
五、索引分类补充
-
主键索引:唯一,非空,聚簇索引。
-
唯一索引:索引值唯一,可以 null。
-
普通索引:无唯一性约束。
-
联合索引:多个字段组合索引。
-
前缀索引:长字符串,取前 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(重做日志)
- 作用 :保证事务持久性,实现崩溃恢复。
落盘思路:WAL 预写日志,先写日志,再刷数据页。
-
写入流程:事务修改数据页,先写 redo log buffer;事务提交时,redo 写入 os cache,根据
innodb_flush_log_at_trx_commit决定是否刷磁盘。数据库宕机,利用 redo log 把已经提交事务的数据恢复到磁盘。 -
性质:物理日志,记录 "某个数据页被改成什么内容"。
-
文件:固定大小环形文件组(ib_logfile0、ib_logfile1),循环复用。
innodb_flush_log_at_trx_commit三值:
-
1(默认):事务提交,redo log 刷磁盘。最安全,性能略低。
-
0:每秒刷盘,事务提交不刷。宕机丢 1s 数据。
-
2:事务提交写入系统缓存,每秒刷盘。宕机可能丢系统缓存内数据。
二、undo log(回滚日志)
-
作用 :①事务原子性,失败时回滚;②构建版本链,支撑 MVCC 实现快照读。
-
性质:逻辑日志,记录数据修改前旧版本。不是物理页还原,是反向操作(insert 对应 delete,update 反向 update)。
-
生命周期:事务提交不会立刻删除 undo;MVCC 还有事务需要读取旧版本时,undo 保留;没有事务依赖后,后台 purge 线程清理。
redo 是存修改后的数据;undo 存修改前旧数据。
三、binlog(二进制日志)
-
作用 :MySQL 服务层面日志,用于主从复制、数据备份恢复。不属于 InnoDB,是 Server 层日志。
-
性质:逻辑日志,记录 SQL 语句 / 行变更,记录执行成功后的事件。
-
三种格式:
-
statement:记录原始 SQL,日志体积小;函数、随机函数可能主从不一致。
-
row(生产推荐):记录行数据变更,不依赖 SQL,主从数据一致;日志量大。
-
mixed:混合模式,自动选择 statement/row。
- 写入时机:事务提交阶段写入 binlog cache,提交后刷入 binlog 文件。文件不断追加,不会循环覆盖。
四、两阶段提交(2PC,必考!redo + binlog)
目的:保证 redo log 和 binlog 数据一致,防止主从数据不一致。
-
Prepare 阶段:写 redo log,状态标记 prepare;binlog 写入 cache,不提交。
-
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 最强,性能损耗大。
五、三大日志对比速记
-
redo log:InnoDB 引擎、物理日志、崩溃恢复、环形复用、WAL。
-
undo log:InnoDB 引擎、逻辑日志、回滚 + MVCC。
-
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 个核心线程:
-
IO 线程:和主库建立连接,读取主库 binlog,写入本地 relay log(中继日志)。
-
SQL 线程:读取本地 relay log,解析并执行日志里的 SQL,落地数据。
MySQL8.0 后,SQL 单线程瓶颈优化为并行复制。
二、完整复制流程
-
Master 开启 binlog,所有 DML/DDL 操作写入 binlog。
-
Slave 的 IO 线程连接 Master,请求指定位置之后的 binlog。
-
Master 启动 dump 线程,推送 binlog 事件给 Slave IO 线程。
-
IO 线程接收 binlog,写入本机 relay log 中继日志。
-
Slave 的 SQL 线程读取 relay log,解析执行,同步数据。
-
从库记录已经同步到的 binlog 位置,下次断点续传。
三、复制模式(3 种)⭐高频
1.异步复制(默认)
主库写完 binlog 直接返回客户端,不等待从库同步。
优点:性能高;
缺点:主库宕机,binlog 还没传到从库,数据丢失。
2.半同步复制(semi-sync)
主库提交后,等待至少一台从库接收 binlog 并 ack 确认,再返回客户端成功。
不是等从库执行完,只是等从库收到日志。
优点:降低丢数据风险;
缺点:有等待延迟,性能下降。 两个超时分支:超时自动降级为异步。
3.并行复制
解决老版本 SQL 单线程回放 relay log,主从延迟大的问题。
原理:同一事务组的事务,可以并行回放。MySQL8.0 增强。
四、binlog 三种格式回顾(主从重点)
-
statement:记录 SQL 语句,日志小;函数、rand () 等场景主从数据不一致。
-
row(生产推荐):记录行变更,不依赖 SQL 上下文,主从一致性强;日志体积更大。
-
mixed:混合模式,自动选择 statement/row。
五、主从延迟(面试高频)
产生原因
-
主库写入并发高,从库 SQL 线程回放速度跟不上;旧版本单线程回放。
-
从库硬件弱、索引缺失,大事务(大批量 update)。
-
网络延迟,大 binlog 传输慢。
-
主库大事务,binlog 一次性推送,从库长时间执行。
解决方案
-
开启并行复制,提升从库回放能力
-
优化大事务,拆分事务,避免一次性生成超大 binlog
-
主从硬件匹配,从库建好索引
-
选用 row 格式 binlog;网络优化
-
业务规避:不要在从库跑大量复杂查询,抢占资源
六、主从数据不一致场景
-
主从复制延迟,查询从库读到旧数据
-
异步复制,主宕机切换,部分 binlog 未同步
-
从库人为写入数据(禁止从库写)
-
binlog 格式问题,statement 模式下非确定性函数
七、GTID(全局事务 ID)⭐重点
GTID:Global Transaction ID,全局唯一事务编号,uuid:事务号。
作用:
-
主从切换时,不用手动找 binlog 文件名 + position,自动定位同步位点。
-
保证同一个事务只在从库执行一次,防止重复回放。
故障切换场景,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 常见原因(背诵)
-
没建索引,触发全表扫描 ALL
-
索引失效:函数运算、隐式转换、like 左模糊、or、破坏最左前缀
-
大事务:单次事务处理海量数据,锁等待、binlog 暴涨,主从延迟
-
索引过多:写入(insert/update/delete)维护索引开销大
-
查询返回大量数据:select *,一次性查出几十万行,网络 + 内存压力
-
join 多表关联,关联字段无索引
-
order by、group by 字段无索引,触发 Using filesort / Using temporary
-
锁等待:行锁被其他事务持有,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)业务层面优化
-
大事务拆小,不要一次更新几万条数据,减少锁持有时间,降低主从延迟
-
冷热数据分离,历史数据归档,单表数据量过大做分库分表
-
读多写少场景,引入 Redis 缓存,减轻 DB 压力
-
读写分离,读请求走从库;注意主从延迟问题
(4)参数层面优化
-
innodb_buffer_pool_size:缓存热点数据页,服务器内存 50%~70%
-
sort_buffer_size、join_buffer_size:单会话缓冲区,不要调太大,容易内存暴涨
-
innodb_flush_log_at_trx_commit,根据业务安全要求权衡性能
五、锁等待类慢 SQL 排查思路
SQL 本身执行很快,但是等待锁,耗时很长:
-
show engine innodb status; 查看事务、锁等待信息
-
找到持有锁的长事务,kill 会话
-
缩短事务执行时间,事务内不要放外部接口调用
高频深挖面试题
Q1:limit 大偏移量为什么慢?
数据库要扫描并丢弃前面 offset 条记录,才返回后面数据。用主键 ID 过滤优化。
Q2:Using filesort 一定是磁盘排序吗?
不一定。优先在内存排序,内存不够才落地磁盘;无论内存还是磁盘,都代表没有索引排序,需要优化。
Q3:为什么不建议 select * ?
1.读取多余字段,IO 增加;
-
无法触发覆盖索引,发生回表;
-
网络传输量大。
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(多个表自增会重复)。 方案:
-
雪花算法 Snowflake:64bit,时间戳 + 机器号 + 序列号;本地生成,高性能。缺点依赖系统时钟,时钟回拨会重复。
-
数据库号段模式:预分配一段 ID,用完再取下一段。
-
Redis 自增生成 ID。
(3)分布式事务
跨库操作,本地事务失效。 方案:
-
最终一致性:TCC、SAGA、本地消息表、事务消息(RocketMQ)
-
强一致性:XA 2PC,性能差,生产极少用。
业务优先:尽量设计成单分片事务,规避分布式事务。
(4)分页、排序、聚合
order by / limit跨分片:每个分片单独查询,应用内存汇总再排序分页。 大 limit 分页性能极差。 优化:
-
带上分片 key 查询;
-
避免深度分页;
-
时间 / ID 条件过滤,减少汇总数据。
(5)扩容迁移问题
取模分片最大痛点:节点增加,取模结果变化,大量数据需要搬迁。 方案:
-
双写迁移:旧分片继续写入,同步迁移数据,数据对齐后切换路由,灰度上线。
-
预分片,预留足够分片数。
(6)全局唯一约束
单库唯一索引失效。例如手机号唯一,数据分布多个分片,数据库无法全局校验。
解决:通过 Redis / 分布式锁做唯一性校验。
四、分库分表落地原则(背诵)
-
能不分就不分。优先索引优化、读写分离、冷热归档。千万以下不轻易拆分。
-
分片 key 慎重选择,尽量让同一业务数据落在同一个分片,减少跨分片查询。
-
避免跨库事务、跨库 join。
-
预估未来数据量,提前规划分片数量,减少后续扩容成本。
高频深挖面试题
Q1:垂直拆分和水平拆分区别?
垂直:按业务 / 字段拆分,表结构不同;水平:按行拆分,表结构相同。
Q2:雪花算法时钟回拨怎么解决?
记录上一次生成 ID 的时间戳;如果当前时间小于上次时间,等待时钟追上,或抛出异常。
Q3:分片 key 为什么很重要?
查询不带分片 key,会触发全分片广播查询,性能暴跌。
Q4:分库分表和读写分离区别?
读写分离:同一套数据,主写从读,解决读压力。 分库分表:数据切割到多个库,解决单库容量、读写瓶颈。两者可以一起使用。
Q5:什么场景不适合分库分表?
查询条件不固定,大量不带分片 key 的查询;大量跨分片 join;业务简单,数据量不大。
一句话背诵总结
分库分表分为垂直(业务 / 字段)、水平(数据行)拆分;
分片策略有取模、范围、一致性 hash;
痛点集中在跨分片 join、分布式 ID、分布式事务、分页排序、数据扩容迁移、全局唯一约束;
落地原则优先优化索引、读写分离,尽量避免跨分片操作,能不分则不分。