MySQL 深度¶
一句话:MySQL 优化的核心是"让查询走对索引、让数据分得开、让慢 SQL 无处藏身",是高并发链路压低 P99 延迟的关键一环。
概念¶
关系型数据库是大多数交易系统的存储基石。它在高并发下的瓶颈通常是:单表数据量过大导致 B+ 树层级加深与扫描变慢、索引设计不合理导致全表扫描、锁与隔离级别引发的性能下降。
资深工程师对 MySQL 的掌握体现在三个层次:底层(B+ 树为什么、聚簇索引与非聚簇索引、回表与覆盖索引)、设计(分库分表如何选分片键、跨分片难题如何解)、治理(慢 SQL 如何发现与系统性治理,把核心接口 P99 降下来)。报告量化"核心接口 P99 延迟下降 50%"正是这条链路的典型成果。
原理¶
InnoDB 索引:B+ 树¶
InnoDB 的索引数据结构是 B+ 树。理解 B+ 树要抓住三个特征:
- 非叶子节点只存索引(键),不存数据:一个节点能放很多键,树更矮,磁盘 IO 次数少。
- 所有数据都存放在叶子节点,且叶子节点之间用双向链表相连:范围查询只需定位起点,沿链表顺序扫描即可。
- 三层 B+ 树可支撑约千万级行:根节点常驻内存,每次查询约 3 次磁盘 IO。
为什么是 B+ 树而不是 B 树/红黑树/Hash:
| 结构 | 劣势(为何不用) |
|---|---|
| Hash | 不支持范围查询、排序,O(1) 但场景受限 |
| 红黑树/二叉树 | 树高随数据量线性增长,磁盘 IO 太多 |
| B 树 | 非叶子节点也存数据,扇出小、树更高;范围查询需中序遍历回溯 |
| B+ 树 | 扇出大、树矮(3 层千万级)、叶子链表支持高效范围扫描 |
聚簇索引与回表¶
InnoDB 的聚簇索引:数据行本身按主键顺序存储在 B+ 树叶子节点,主键索引即数据。二级索引(非聚簇)叶子节点存的是主键值,查到主键后还需回表到聚簇索引取完整行。
覆盖索引:查询的列全部被某个索引覆盖,无需回表,EXPLAIN 显示 Extra: Using index。这是优化高频查询的利器。
执行计划 EXPLAIN¶
| 字段 | 关注点 |
|---|---|
type |
访问类型,const > eq_ref > ref > range > index > ALL,ALL 是全表扫描(红旗) |
key |
实际使用的索引;NULL 表示没用索引 |
rows |
预估扫描行数,越小越好 |
Extra |
Using index(覆盖索引,好)、Using filesort(额外排序,警惕)、Using temporary(临时表,警惕) |
分库分表(ShardingSphere)¶
单表数据量达到千万级、单库 QPS 触顶时,需要水平拆分。ShardingSphere 是主流的 Java 分库分表中间件(Sharding-JDBC 嵌入式、Sharding-Proxy 独立代理)。
分片键选择是核心决策。报告场景采用按 user_id 水平拆分,因为:
- 交易链路绝大多数查询都带
user_id(查订单、查账户),按 user_id 分片能让"同一用户的数据落在同一分片",避免跨分片。 user_id离散度高、分布均匀,数据不会倾斜。
分片策略通常配合分库(分实例)+ 分表(同实例多表):先按 user_id 取模分到 N 个库,再在每个库内取模分到 M 张表。
跨分片难题¶
分库分表带来三个典型难题:
| 难题 | 成因 | 解法 |
|---|---|---|
| 跨分片 JOIN | 数据在不同库,无法直接 JOIN | 应用层组装、冗余字段、绑定表(ShardingSphere binding table) |
| 跨分片分页 | LIMIT offset, size 需要每个分片取 offset+size 再归并,深度分页性能差 |
游标分页/带上一页末尾 ID、二次查询、ES 宽表 |
| 全局唯一 ID | 各分片自增会冲突 | 雪花算法(Snowflake)、号段模式(Leaf) |
慢 SQL 治理与 P99¶
慢 SQL 是 P99 劣化的主因之一。治理闭环:
- 采集:开启 MySQL 慢查询日志(
long_query_time)+ APM(SkyWalking/Pinpoint)抓接口 SQL RT。 - 分析:
EXPLAIN看执行计划,识别全表扫描、未命中索引、回表过多、filesort。 - 优化:加合适的联合索引(最左前缀)、用覆盖索引消除回表、改写 SQL(避免
SELECT *、避免索引列上函数运算)。 - 验证:压测对比 P99,确认有效。
实战要点¶
结合报告"分库分表(ShardingSphere 按 user_id)+ 慢 SQL 治理,核心接口 P99 下降 50%"的经验:
-
分片键一定要贴合主查询路径:选
user_id是因为 90% 以上的交易查询都带它;若选了order_id,则"查某用户所有订单"必跨分片。分片键选错是分库分表最大的设计债,后期几乎无法回退。 -
联合索引遵守最左前缀与区分度:
(user_id, create_time)而不是(create_time, user_id);高区分度列放前面。避免在索引列上做函数(WHERE DATE(create_time)=...)会失效,改范围查询。 -
深度分页用游标:
LIMIT 100000, 20仍要扫描前 100020 行。改WHERE id > last_id ORDER BY id LIMIT 20(游标分页),P99 可从数百毫秒降到个位数毫秒。 -
建立慢 SQL 监控与治理闭环:APM 自动抓取 Top10 慢 SQL,每周治理,把单条 SQL RT 压到阈值内,是 P99 持续下降的系统性工程,而非一次性优化。
-
分库分表不是银弹,先优化再拆:单表能扛就不要拆。拆之前先穷尽索引优化、读写分离、归档冷数据。一旦拆了,跨分片统计、JOIN、事务(变成分布式)都会带来复杂度。
-
跨分片统计用 ES 宽表/离线数仓:实时大盘统计不要在分片 MySQL 上做(要查全部分片再聚合),落到 ES 或离线 Hive。
本节相关题目¶
| 难度 | 题目 | 链接 |
|---|---|---|
| 基础 | InnoDB 索引数据结构、为何 B+ 树 | → 题库 |
| 进阶 | 分库分表后跨分片 JOIN 与分页 | → 题库 |
| 深度 | 不停机分片扩容迁移方案 | → 题库 |
| 深度 | "核心接口 P99 -50%"统计口径与归因 | → 题库 |