MySQL 核心技术详解:从索引到底层架构
深入解析 MySQL InnoDB 存储引擎的核心机制,包括索引原理、查询优化、主从复制、事务与锁机制,以及 MHA 高可用方案。
MySQL 是互联网应用中最常用的关系型数据库之一。本文将系统梳理 MySQL 核心技术,帮助深入理解其底层原理。
一、索引原理
为什么需要索引?
MySQL 常用 InnoDB 存储引擎,实际场景中读写比例约为 10:1,查询往往成为性能瓶颈。索引的核心作用是加速查询速度。
可以类比书籍的目录:没有目录时,查找内容需要逐页翻阅;有了目录,可以直接定位到目标页码。
索引的本质
索引是 MySQL 中的一种数据结构,它将随机 I/O 转换为顺序 I/O。代价是写入操作时需要维护索引结构,因此会略微降低写入性能。
索引的数据结构演进
InnoDB 的索引主要基于 B+ 树 实现。让我们看看数据结构的演进过程:
1. 二叉树(Binary Tree)
每个节点最多有两个子节点,左子节点小于当前节点,右子节点大于当前节点。
问题:当数据有序插入时,树会退化成链表,查询效率退化为 O(n)。
5
/
4
/
32. 平衡二叉树(AVL Tree)
增加平衡条件:左右子树高度差 ≤ 1,解决了链表退化问题。
问题:每个节点只存一个键值,当数据量大时,树的高度依然很高,导致 I/O 次数过多。
3. B 树(Balance Tree)
多路平衡查找树,特点:
- 每个节点可以有多个子节点
- 每个节点存储多个键值和数据
- 所有叶子节点在同一层
问题:
- 数据分散在树的各个节点中
- 范围查询效率低:查完一个节点后需要回到父节点才能访问下一个节点
4. B+ 树(InnoDB 实际使用的结构)
B 树的改进版,InnoDB 索引的核心数据结构:
特点一:数据分离存储
- 非叶子节点只存 key(索引键)
- 叶子节点存 key + value(完整数据或主键)
- 非叶子节点能存储更多 key,树更矮,I/O 效率更高
特点二:叶子节点双向链表
- 所有叶子节点通过指针相互连接
- 范围查询时,找到起始点后可直接通过链表遍历
[10 | 30]
/ | \
[1|3|5|7] [15|20|25] [35|40|45]
↕️ ↕️ ↕️
叶子节点双向链表连接索引类型
聚集索引(Clustered Index)
以主键为 key 构建的 B+ 树,叶子节点存放完整的行数据。表数据本身就是一颗 B+ 树。
-- 通过主键查询最快,直接命中聚集索引
SELECT * FROM users WHERE id = 100;辅助索引(Secondary Index)
叶子节点只存储索引列的值和对应的主键 ID。查询非索引列时需要先查到主键,再回到聚集索引查找完整数据,这个过程叫做回表。
-- 假设有 INDEX idx_name (name)
SELECT * FROM users WHERE name = 'sirius';
-- 1. 在辅助索引找到 name='sirius' 对应的主键 id
-- 2. 用 id 去聚集索引回表查询完整数据联合索引与最左前缀原则
在多个列上建立的索引:
CREATE INDEX idx_name_age ON users(name, age);B+ 树按照从左到右的顺序排序:先按 name 排序,name 相同再按 age 排序。
最左前缀原则详解
"最左"指的是索引定义中的最左列,不是 WHERE 子句的书写顺序。
索引定义:CREATE INDEX idx_name_age ON users(name, age);
↑ ↑
最左列 第二列
B+ 树物理存储顺序(先 name,后 age):
[name:A, age:18]
[name:A, age:25] ← name 相同,按 age 排序
[name:B, age:20]
[name:C, age:30]查询匹配规则
-- ✅ 能用索引:包含最左列 name
WHERE name = 'sirius'
WHERE name = 'sirius' AND age = 24
-- ✅ 也能用索引:MySQL 优化器会自动调整顺序
WHERE age = 24 AND name = 'sirius' -- 优化器重写成 name 在前
-- ❌ 用不上索引:缺少最左列 name
WHERE age = 24
-- ❌ 用不上 age 部分:name 用了范围查询后,age 无法使用索引
WHERE name LIKE 's%' AND age = 24
-- 原因:name LIKE 's%' 匹配多个值,每个值下的 age 是有序的,
-- 但跨不同 name 值,age 不是全局有序的索引下推(Index Condition Pushdown, ICP)
MySQL 5.6 引入的优化,在回表之前就利用索引中的字段进行条件过滤,减少回表次数。
-- 索引:idx_name_age (name, age)
SELECT * FROM users WHERE name LIKE 's%' AND age = 24;
-- 没有 ICP:先回表获取所有 name LIKE 's%' 的数据,再过滤 age
-- 有 ICP:直接在索引中过滤 age,只回表符合条件的数据二、索引优化
1. 选择合适的列建立索引
- 区分度高的列:如用户 ID、邮箱等
- 避免低区分度列:如性别(只有男/女)、状态枚举值
2. 联合索引设计原则
- 按查询条件频率从高到低排列列顺序
- 优先把等值查询条件列放在前面
3. 覆盖索引
覆盖索引是一种查询优化状态,不是独立的索引类型。
核心概念
| 概念 | 说明 |
|---|---|
| 索引类型 | 主键索引、唯一索引、普通索引、联合索引(这些是物理存在的) |
| 覆盖索引 | 查询时的一种优化状态:索引包含所有查询字段,无需回表 |
同一个索引,不同查询,可能是覆盖索引,也可能不是:
-- 索引:CREATE INDEX idx_name_age ON users(name, age);
-- ❌ 不是覆盖索引:需要回表查 email
SELECT name, age, email FROM users WHERE name = 'sirius';
-- ✅ 是覆盖索引:name 和 age 都在索引里
SELECT name, age FROM users WHERE name = 'sirius';图解对比
┌─────────────────────────────────────────────────────────────┐
│ 普通查询(需要回表) │
├─────────────────────────────────────────────────────────────┤
│ SELECT * FROM users WHERE name = 'sirius'; │
│ │
│ 1. 辅助索引找到 name='sirius' → 拿到 id=50 │
│ 2. 用 id=50 去聚集索引回表 → 获取完整行数据 │
│ │
│ 结果:访问 2 颗 B+ 树,两次索引查找 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 覆盖索引查询(无需回表) │
├─────────────────────────────────────────────────────────────┤
│ SELECT name, age FROM users WHERE name = 'sirius'; │
│ │
│ 1. 联合索引 idx_name_age 中找到 name='sirius' │
│ 2. 索引中直接包含 name 和 age,直接返回 │
│ │
│ 结果:只访问 1 颗 B+ 树,减少一次回表 I/O │
└─────────────────────────────────────────────────────────────┘如何判断是否使用覆盖索引
EXPLAIN SELECT name, age FROM users WHERE name = 'sirius';
-- Extra 列显示 "Using index" 表示使用了覆盖索引
### 4. 利用索引有序性
字符串查询时,优先使用前缀匹配:
```sql
-- ✅ 能用索引
WHERE name LIKE 'sirius%'
-- ❌ 不能用索引(% 在开头)
WHERE name LIKE '%sirius'5. 其他优化建议
| 建议 | 说明 |
|---|---|
| 索引字段不参与计算 | WHERE id + 1 = 100 会导致索引失效 |
| 限制索引数量 | 索引占空间,更新表时维护成本高 |
| 优先使用连接代替子查询 | 子查询往往产生临时表 |
| 小表驱动大表 | JOIN 时让小表作为驱动表 |
三、慢查询优化
定位慢查询
-- 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 使用 EXPLAIN 分析执行计划
EXPLAIN SELECT * FROM users WHERE name = 'sirius';EXPLAIN 关键字段
| 字段 | 说明 |
|---|---|
type | 访问类型,从优到差:system > const > eq_ref > ref > range > index > ALL |
key | 实际使用的索引 |
rows | 扫描的行数,越小越好 |
Extra | 额外信息,Using index 表示覆盖索引,Using where 表示回表过滤 |
优化步骤
- 检查是否命中索引:
key列为 NULL 表示没走索引 - 判断查询类型:
- 等值查询:检查是否是小字段、非空且唯一
- 范围查询:尽量缩小范围,大数据量分页获取
- 使用最左前缀:
LIKE 'sirius%'而非'%sirius' - 优化 SQL 逻辑:能 JOIN 就不要子查询
终极方案:分库分表
当单表数据量过大(通常超过千万级)时,考虑分库分表:
垂直拆分
- 垂直分库:按业务将表分散到不同数据库
- 垂直分表:将热点字段和冷数据字段分开存储
水平拆分
- 水平分库:将数据分散到不同数据库实例,如 ID 1-1000 在库1,1001-2000 在库2
- 水平分表:将数据分散到结构相同的多个表中
四、主从复制
复制原理
一句话:主库做什么,从库就跟着做什么。将主服务器的 binlog 日志复制到从服务器执行,达到数据一致。
┌─────────┐ binlog ┌─────────┐
│ Master │ ─────────────→ │ Slave │
│ (主库) │ │ (从库) │
└─────────┘ └─────────┘
↓ ↓
Dump Thread I/O Thread → SQL Thread
(发送日志) (接收日志 → 重放执行)详细流程:
- 从库 I/O 线程连接主库,请求 binlog
- 主库 Dump 线程发送 binlog 到从库
- 从库 I/O 线程将日志写入 relay-log(中继日志)
- 从库 SQL 线程读取 relay-log 并执行
复制模式
异步复制(默认)
主库提交事务后立即返回客户端,异步发送 binlog 到从库。
- 优点:性能最优
- 缺点:主库宕机可能丢失数据
半同步复制
主库提交事务后,等待至少一个从库确认收到 binlog,再返回客户端。
- 优点:数据可靠性更高
- 缺点:有一定性能损耗
异步复制追求极致性能,半同步复制在性能和数据一致性之间做权衡。
与两阶段提交
InnoDB 有三个重要日志:
| 日志 | 作用 | 内容 |
|---|---|---|
| Undo Log | 回滚旧数据 | 修改前的数据 |
| Redo Log | 崩溃恢复 | 修改后的数据 |
| Binlog | 主从同步 | SQL 语句或行变更 |
两阶段提交解决 Redo Log 和 Binlog 不一致问题:
阶段1:写入 Redo Log,标记为 prepare
阶段2:写入 Binlog
阶段3:Redo Log 标记为 commit崩溃恢复:
- Redo 为 prepare,Binlog 未写入 → 回滚
- Redo 为 prepare,Binlog 已写入 → 提交
五、事务与锁机制
ACID 特性
| 特性 | 保证机制 | 说明 |
|---|---|---|
| 原子性 | Undo Log | 事务要么全部完成,要么全部回滚 |
| 一致性 | 最终目标 | 由原子性、隔离性、持久性共同保证 |
| 隔离性 | 锁 + MVCC | 事务之间互不干扰 |
| 持久性 | Redo Log | 事务提交后修改永久生效 |
隔离级别与并发问题
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | ✗ | ✗ | ✗ |
| Read Committed | ✓ | ✗ | ✗ |
| Repeatable Read(默认) | ✓ | ✓ | ✗(InnoDB解决) |
| Serializable | ✓ | ✓ | ✓ |
- 脏读:读到其他未提交事务的数据
- 不可重复读:同一事务内两次读取同一行数据结果不同
- 幻读:同一事务内两次按相同条件查询,记录数不同
InnoDB 锁机制
行级锁
| 锁类型 | 说明 |
|---|---|
| 记录锁(Record Lock) | 锁住一条索引记录 |
| 间隙锁(Gap Lock) | 锁住索引区间,开区间,解决幻读 |
| 临键锁(Next-Key Lock) | 记录锁 + 间隙锁,左开右闭,InnoDB 默认行锁 |
| 意向锁 | 表级锁,快速判断表中是否有行锁冲突 |
死锁场景及解决
场景一:顺序不一致
-- 事务A -- 事务B
UPDATE A SET ... UPDATE B SET ...
UPDATE B SET ... UPDATE A SET ...
-- 循环等待,形成死锁解决:约定相同顺序访问资源,如总是先更新 ID 小的记录。
场景二:间隙锁冲突
当前读的间隙锁与插入操作冲突。
解决:尽量缩小锁定范围,避免大范围 GAP LOCK。
死锁解决机制:InnoDB 有死锁检测,会自动回滚代价较小的事务。
乐观锁 vs 悲观锁
| 类型 | 思想 | 适用场景 |
|---|---|---|
| 悲观锁 | "总会有人改",先加锁再访问 | 写多读少,强一致性场景 |
| 乐观锁 | "没人会改",更新时检查版本 | 读多写少,并发不激烈场景 |
-- 乐观锁示例:使用版本号
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;六、MHA 高可用方案
MHA(Master High Availability)是 MySQL 主从故障自动切换方案。
核心组件
| 组件 | 角色 | 部署位置 |
|---|---|---|
| Manager | 监控主库、协调故障转移 | 独立节点(建议非数据库节点) |
| Node | 执行 Manager 指令(复制切换、日志补全) | 所有 MySQL 服务器 |
工作原理
1. 监控阶段
- Manager 定期执行
SELECT 1检测主库(默认 3 秒,可配置ping_interval)
2. 故障检测
- 主库无响应时,从其他 Node 发起 SSH 和 MySQL 连接验证
- 确认主库故障后开始故障转移
3. 故障转移流程
1. 选举数据最新的从库(对比 Relay_Log_File 和 Relay_Log_Pos)
2. 从旧主库 binlog 中提取未同步日志
3. 应用缺失的 binlog 到新主库
4. 提升从库为新主库
5. 其他从库 CHANGE MASTER TO 指向新主库
6. VIP 漂移到新主库耗时:网络正常时,通常 10-30 秒完成切换。
七、核心概念总结
索引相关
| 概念 | 一句话总结 |
|---|---|
| B+ 树 | 非叶子节点存 key,叶子节点存 key+data 并形成双向链表 |
| 聚集索引 | 以主键构建,叶子节点存完整行数据,表数据本身就是 B+ 树 |
| 辅助索引 | 叶子节点存索引列值+主键 ID,查询非索引列需要回表 |
| 覆盖索引 | 索引包含查询所有字段,无需回表(是一种优化状态,不是索引类型) |
| 最左前缀 | 联合索引必须从定义的最左列开始匹配,与 WHERE 顺序无关 |
| 索引下推 | 将 WHERE 条件判断下推到存储引擎层,减少回表次数 |
索引优化 checklist
□ 是否命中索引?(EXPLAIN 看 key 列)
□ 是否使用最左前缀?
□ 是否能使用覆盖索引减少回表?
□ 范围查询是否尽量缩小范围?
□ 字符串查询是否使用前缀匹配(LIKE 'prefix%')?
□ 索引列是否避免函数运算和隐式类型转换?主从复制核心
异步复制:性能好,可能丢数据
半同步复制:等从库确认,平衡性能与可靠性
两阶段提交:Redo Log prepare → Binlog → Redo Log commit事务与锁
| 特性/机制 | 实现方式 |
|---|---|
| 原子性 | Undo Log |
| 持久性 | Redo Log |
| 隔离性 | 锁 + MVCC |
| 一致性 | ACID 共同保证 |
| 记录锁 | 锁单行记录 |
| 间隙锁 | 锁区间,防幻读 |
| 临键锁 | 记录锁 + 间隙锁,InnoDB 默认 |
MHA 故障转移流程
1. Manager 检测到主库故障
2. 从各从库选举数据最新的作为新主库
3. 从旧主库 binlog 补全新主库数据
4. 其他从库切换主库指向
5. VIP 漂移到新主库理解这些底层原理,才能在面对复杂的 MySQL 问题时做出正确的优化决策。