← 返回文章列表

MySQL 核心技术详解:从索引到底层架构

深入解析 MySQL InnoDB 存储引擎的核心机制,包括索引原理、查询优化、主从复制、事务与锁机制,以及 MHA 高可用方案。

19 分钟阅读
字号

MySQL 是互联网应用中最常用的关系型数据库之一。本文将系统梳理 MySQL 核心技术,帮助深入理解其底层原理。

一、索引原理

为什么需要索引?

MySQL 常用 InnoDB 存储引擎,实际场景中读写比例约为 10:1,查询往往成为性能瓶颈。索引的核心作用是加速查询速度

可以类比书籍的目录:没有目录时,查找内容需要逐页翻阅;有了目录,可以直接定位到目标页码。

索引的本质

索引是 MySQL 中的一种数据结构,它将随机 I/O 转换为顺序 I/O。代价是写入操作时需要维护索引结构,因此会略微降低写入性能。

索引的数据结构演进

InnoDB 的索引主要基于 B+ 树 实现。让我们看看数据结构的演进过程:

1. 二叉树(Binary Tree)

每个节点最多有两个子节点,左子节点小于当前节点,右子节点大于当前节点。

问题:当数据有序插入时,树会退化成链表,查询效率退化为 O(n)。

    5
   /
  4
 /
3

2. 平衡二叉树(AVL Tree)

增加平衡条件:左右子树高度差 ≤ 1,解决了链表退化问题。

问题:每个节点只存一个键值,当数据量大时,树的高度依然很高,导致 I/O 次数过多。

3. B 树(Balance Tree)

多路平衡查找树,特点:

  • 每个节点可以有多个子节点
  • 每个节点存储多个键值和数据
  • 所有叶子节点在同一层

问题

  1. 数据分散在树的各个节点中
  2. 范围查询效率低:查完一个节点后需要回到父节点才能访问下一个节点

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 表示回表过滤

优化步骤

  1. 检查是否命中索引key 列为 NULL 表示没走索引
  2. 判断查询类型
    • 等值查询:检查是否是小字段、非空且唯一
    • 范围查询:尽量缩小范围,大数据量分页获取
  3. 使用最左前缀LIKE 'sirius%' 而非 '%sirius'
  4. 优化 SQL 逻辑:能 JOIN 就不要子查询

终极方案:分库分表

当单表数据量过大(通常超过千万级)时,考虑分库分表:

垂直拆分

  • 垂直分库:按业务将表分散到不同数据库
  • 垂直分表:将热点字段和冷数据字段分开存储

水平拆分

  • 水平分库:将数据分散到不同数据库实例,如 ID 1-1000 在库1,1001-2000 在库2
  • 水平分表:将数据分散到结构相同的多个表中

四、主从复制

复制原理

一句话:主库做什么,从库就跟着做什么。将主服务器的 binlog 日志复制到从服务器执行,达到数据一致。

┌─────────┐     binlog     ┌─────────┐
│  Master │ ─────────────→ │  Slave  │
│  (主库)  │                │  (从库)  │
└─────────┘                └─────────┘
   ↓                          ↓
Dump Thread               I/O Thread → SQL Thread
(发送日志)                (接收日志 → 重放执行)

详细流程

  1. 从库 I/O 线程连接主库,请求 binlog
  2. 主库 Dump 线程发送 binlog 到从库
  3. 从库 I/O 线程将日志写入 relay-log(中继日志)
  4. 从库 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 问题时做出正确的优化决策。

分享

// RELATED_POSTS

0%