数据库01:MySQL
从 InnoDB 引擎、B+Tree 索引结构、MVCC 到主从复制,拆解 MySQL 高并发读写背后的设计原理和常见调优方向。
MySQL 是互联网公司最常用的关系型数据库,没有之一。很多项目从一开始就用 MySQL,然后在业务增长中不断遇到性能瓶颈、锁冲突、主从延迟等问题。
这篇文章围绕四个核心点:InnoDB 引擎架构、B+Tree 索引、MVCC 并发控制、主从复制,把 MySQL 的设计逻辑和常见坑点讲清楚。
InnoDB 引擎:为什么是默认选择
MySQL 5.5 之后,InnoDB 成为默认存储引擎,不是因为它最完美,而是因为它在事务支持、并发控制、崩溃恢复这三件事上做得最平衡。
| 引擎 | 事务支持 | 锁粒度 | 崩溃恢复 | 适用场景 |
|---|---|---|---|---|
| InnoDB | ACID | 行级锁 | 支持 | 绝大多数业务场景 |
| MyISAM | 不支持 | 表级锁 | 不支持 | 只读、统计报表 |
| Memory | 不支持 | 表级锁 | 不支持 | 临时表、缓存 |
InnoDB 的核心架构可以分成三层:
- Buffer Pool:内存缓存,存放热点数据和索引页,减少磁盘 IO
- Log Buffer + Redo Log:保证事务持久性,先写日志再写磁盘
- Undo Log + Purge:支持 MVCC 和事务回滚
关键设计:WAL(Write-Ahead Logging)。事务提交时,先写 Redo Log,再更新 Buffer Pool,最后后台线程异步刷盘。这样即使崩溃,重启后可以通过 Redo Log 恢复。
B+Tree 索引:为什么快,为什么慢
索引是 MySQL 性能的命门。理解 B+Tree 才能真正明白”为什么这条 SQL 慢”。
B+Tree 结构特点
- 多路平衡树:每个节点可以有多个子节点,树的高度很低(通常 3-4 层)
- 叶子节点链式连接:范围查询只需要遍历叶子节点链表
- 非叶子节点不存数据:只存索引键,减少磁盘 IO
索引类型对比
| 索引类型 | 结构 | 优点 | 缺点 |
|---|---|---|---|
| 主键索引 | B+Tree,叶子节点存整行数据 | 查询最快 | 只能有一个 |
| 普通索引 | B+Tree,叶子节点存主键值 | 灵活 | 需要回表(除覆盖索引) |
| 联合索引 | B+Tree,按索引顺序排列 | 覆盖多条件 | 最左前缀原则 |
| 唯一索引 | B+Tree,键值唯一 | 保证唯一性 | 插入更新开销大 |
最左前缀原则
联合索引 (a, b, c) 能匹配的查询条件:
WHERE a = ?✓WHERE a = ? AND b = ?✓WHERE a = ? AND b = ? AND c = ?✓WHERE b = ?✗(跳过最左列)WHERE a = ? AND c = ?✗(中间列缺失)
索引失效场景
- 索引列上使用函数:
WHERE DATE(create_time) = ? - 隐式类型转换:
WHERE id = '123'(字符串转数字) - 范围查询后列无法使用索引:
WHERE a = ? AND b > ? AND c = ?(c 无法使用) NOT IN/NOT LIKE/!=通常不走索引
MVCC:读写不冲突的魔法
MVCC(Multi-Version Concurrency Control)是 InnoDB 实现高并发的关键,它让读操作不加锁,写操作只锁必要的行。
版本链与 Read View
每个事务看到的数据版本由两个因素决定:
- Undo Log 版本链:每次更新都会生成一条 Undo Log,形成版本链
- Read View:事务开始时创建的快照,包含当前活跃事务 ID
隔离级别与一致性视图
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 读最新数据 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 每次查询生成新 Read View |
| REPEATABLE READ | 不可能 | 不可能 | 不可能 | 事务开始时生成 Read View |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 加锁 |
MySQL 默认隔离级别是 REPEATABLE READ,通过间隙锁(Gap Lock)解决了幻读问题。
快照读 vs 当前读
- 快照读:
SELECT,不加锁,读历史版本 - 当前读:
SELECT ... FOR UPDATE、INSERT、UPDATE、DELETE,加锁,读最新版本
主从复制:读写分离的基础
主从复制是 MySQL 水平扩展的标配,核心思路是”写主库,读从库”。
复制流程
- Binlog Dump:主库记录变更到 Binlog
- IO Thread:从库 IO 线程拉取 Binlog 到 Relay Log
- SQL Thread:从库 SQL 线程回放 Relay Log
复制方式
| 方式 | 同步性 | 延迟 | 适用场景 |
|---|---|---|---|
| 异步复制 | 异步 | 毫秒级 | 绝大多数场景 |
| 半同步复制 | 至少一个从库确认 | 略高 | 对数据一致性要求高 |
| 并行复制 | 多线程回放 | 更低 | 高写入场景 |
常见问题
主从延迟:写多读少、大事务、DDL 操作都会导致延迟。解决方案:
- 小事务,避免长时间锁表
- 并行复制,提高从库回放速度
- 监控延迟,超过阈值自动切主
数据不一致:网络抖动、从库宕机可能导致数据丢失。解决方案:
- 半同步复制,确保至少一个从库落盘
- 定期数据校验(pt-table-checksum)
- 从库只读,防止误写
常见调优方向
硬件层面
- SSD 代替 HDD,随机写性能提升 10 倍以上
- 足够的内存,让热点数据全在 Buffer Pool
- 高速网络,减少主从复制延迟
配置层面
innodb_buffer_pool_size:通常设为物理内存的 50-70%innodb_log_file_size:1-2GB,太大恢复慢,太小切换频繁innodb_flush_log_at_trx_commit:1=安全,2=性能(宕机可能丢 1 秒数据)
架构层面
- 读写分离,主库写,从库读
- 分库分表,突破单库容量上限
- 缓存层(Redis),减少数据库压力
项目判断清单
- 单表数据量超过 1000 万 → 考虑分表或索引优化
- QPS 超过 1 万 → 考虑读写分离或缓存
- 经常出现慢查询 → 先看执行计划,再优化索引
- 主从延迟超过 1 秒 → 检查 Binlog 大小和从库配置
- 事务冲突频繁 → 减少事务粒度,优化锁竞争