本文整合了 InnoDB 存储架构、索引、MVCC、事务、锁机制、三大日志、慢查询、死锁、分布式扩展等核心知识,力求从原理出发,把所有知识点串成一条完整的脉络。
文章目录
-
- [一、InnoDB 存储引擎架构](#一、InnoDB 存储引擎架构)
-
- [1.1 整体架构](#1.1 整体架构)
- [1.2 Buffer Pool](#1.2 Buffer Pool)
- [1.3 Adaptive Hash Index(自适应哈希索引)](#1.3 Adaptive Hash Index(自适应哈希索引))
- [1.4 Change Buffer](#1.4 Change Buffer)
- [1.5 表空间物理结构](#1.5 表空间物理结构)
- [1.6 InnoDB 数据页物理结构](#1.6 InnoDB 数据页物理结构)
- [二、B+ 树索引体系](#二、B+ 树索引体系)
-
- [2.1 为什么选择 B+ 树](#2.1 为什么选择 B+ 树)
- [2.2 聚簇索引与二级索引](#2.2 聚簇索引与二级索引)
- [2.3 覆盖索引](#2.3 覆盖索引)
- [2.4 联合索引与最左前缀原则](#2.4 联合索引与最左前缀原则)
- [2.5 索引下推(ICP)](#2.5 索引下推(ICP))
- [2.6 B+ 树候选项淘汰推导](#2.6 B+ 树候选项淘汰推导)
- [2.7 页分裂与页合并](#2.7 页分裂与页合并)
- [2.8 索引失效的本质原因](#2.8 索引失效的本质原因)
- [三、MVCC 多版本并发控制](#三、MVCC 多版本并发控制)
-
- [3.1 核心组件](#3.1 核心组件)
- [3.2 可见性判断算法](#3.2 可见性判断算法)
- [3.3 RC 与 RR 的 ReadView 差异](#3.3 RC 与 RR 的 ReadView 差异)
- [3.4 执行示例详解](#3.4 执行示例详解)
- [3.5 快照读与当前读](#3.5 快照读与当前读)
- 四、锁机制
-
- [4.1 锁分类总览](#4.1 锁分类总览)
- [4.2 意向锁](#4.2 意向锁)
- [4.3 Record Lock(记录锁)](#4.3 Record Lock(记录锁))
- [4.4 Gap Lock(间隙锁)](#4.4 Gap Lock(间隙锁))
- [4.5 Next-Key Lock](#4.5 Next-Key Lock)
- [4.6 MDL 锁(元数据锁)](#4.6 MDL 锁(元数据锁))
- [4.7 AUTO-INC 锁](#4.7 AUTO-INC 锁)
- 五、三大日志
-
- [5.1 Redo Log(重做日志)](#5.1 Redo Log(重做日志))
- [5.2 Undo Log(回滚日志)](#5.2 Undo Log(回滚日志))
- [5.3 Binlog(归档日志)](#5.3 Binlog(归档日志))
- [5.4 两阶段提交](#5.4 两阶段提交)
- [5.5 三大日志协作流程](#5.5 三大日志协作流程)
- 六、事务与隔离级别
-
- [6.1 ACID 特性](#6.1 ACID 特性)
- [6.2 四种隔离级别](#6.2 四种隔离级别)
- [6.3 秒杀场景串联事务全貌](#6.3 秒杀场景串联事务全貌)
- 七、死锁
-
- [7.1 死锁产生条件](#7.1 死锁产生条件)
- [7.2 典型死锁场景](#7.2 典型死锁场景)
- [7.3 死锁处理](#7.3 死锁处理)
- 八、慢查询诊断
-
- [8.1 慢查询分类与优化](#8.1 慢查询分类与优化)
- [8.2 EXPLAIN 关键字段](#8.2 EXPLAIN 关键字段)
- [九、分布式 MySQL](#九、分布式 MySQL)
-
- [9.1 三类分布式需求](#9.1 三类分布式需求)
- [9.2 主从复制原理](#9.2 主从复制原理)
- [9.3 InnoDB Cluster](#9.3 InnoDB Cluster)
- [9.4 NDB Cluster](#9.4 NDB Cluster)
- [9.5 生产选型参考](#9.5 生产选型参考)
- 十、分库分表与平滑迁移
-
- [10.1 分片策略](#10.1 分片策略)
- [10.2 分片后的挑战](#10.2 分片后的挑战)
- [10.3 平滑迁移方案](#10.3 平滑迁移方案)
- 十一、字段类型选择最佳实践
-
- [11.1 字符串类型](#11.1 字符串类型)
- [11.2 数值类型](#11.2 数值类型)
- [11.3 时间类型](#11.3 时间类型)
- [11.4 布尔/状态类型](#11.4 布尔/状态类型)
- 十二、高并发数据库设计分层
-
- [12.1 连接层优化](#12.1 连接层优化)
- [12.2 缓存层](#12.2 缓存层)
- [12.3 SQL 优化层](#12.3 SQL 优化层)
- [12.4 架构层](#12.4 架构层)
- 总结
一、InnoDB 存储引擎架构
1.1 整体架构
InnoDB 采用「内存 + 磁盘」的双层架构。数据页在磁盘上以 B+ 树形式组织,通过 Buffer Pool 缓存到内存中加速读写,配合 WAL(Write-Ahead Logging)机制保证持久性。
┌───────────────────────────────────────────────────────────┐
│ 内存层 │
│ ┌───────────┐ ┌──────────┐ ┌─────────────┐ │
│ │Buffer Pool│ │ Log Buffer│ │Change Buffer│ │
│ └───────────┘ └──────────┘ └─────────────┘ │
│ ┌───────────────────────┐ │
│ │ Adaptive Hash Index │ │
│ └───────────────────────┘ │
├───────────────────────────────────────────────────────────┤
│ 磁盘层 │
│ ┌──────────────┐ ┌────────┐ ┌────────┐ ┌──────┐ │
│ │.ibd 表空间文件│ │Redo Log│ │Undo Log│ │Binlog│ │
│ └──────────────┘ └────────┘ └────────┘ └──────┘ │
└───────────────────────────────────────────────────────────┘
1.2 Buffer Pool
Buffer Pool 是 InnoDB 最核心的内存组件,默认 128MB(生产环境通常设为物理内存的 60%~80%)。它缓存数据页和索引页,使用改良的 LRU 链表管理页面淘汰。
LRU 链表分为 young 区和 old 区(默认 5:3 比例),新读入的页先放到 old 区头部,只有在 old 区停留超过 innodb_old_blocks_time(默认 1000ms)后再被访问才会晋升到 young 区。这样可以避免全表扫描等一次性大量读入的页面冲刷掉真正的热点数据。
脏页刷新机制:当数据页被修改后变为脏页,InnoDB 通过以下方式将脏页刷回磁盘:Redo Log 空间不足时强制刷盘、后台线程定期刷盘(自适应刷盘算法根据脏页比例和 redo log 生成速度动态调整)、Buffer Pool 空间不足时淘汰脏页。
1.3 Adaptive Hash Index(自适应哈希索引)
InnoDB 会监控索引的访问模式,如果发现某个索引页被频繁地进行等值查询(如 WHERE id = 5),会自动在内存中为该页建立哈希索引,将 B+ 树查找的 O(log n) 降为 O(1)。
这是完全自动化的优化,无需手动干预,也不能手动指定。可通过 innodb_adaptive_hash_index 参数开关,通过 SHOW ENGINE INNODB STATUS 查看命中率。在高并发等值查询场景下效果显著,但在范围查询为主的场景下意义不大,且会占用 Buffer Pool 空间。
1.4 Change Buffer
当对非唯一二级索引执行 INSERT/UPDATE/DELETE 时,如果目标页不在 Buffer Pool 中,InnoDB 不会立即从磁盘读入该页,而是将变更缓存到 Change Buffer 中。等到后续有查询真正需要该页时,再将其读入并与 Change Buffer 中的变更合并(merge)。
Change Buffer 适用场景:写多读少的业务,且二级索引为非唯一索引。对于唯一索引,由于需要检查唯一性约束,必须将页读入内存,因此无法使用 Change Buffer。
1.5 表空间物理结构
InnoDB 的磁盘存储从大到小分为四层:
表空间(Tablespace) ------ .ibd 文件
└── 段(Segment):数据段、索引段、回滚段
└── 区(Extent):64 个连续页,共 1MB
└── 页(Page):16KB,最小 I/O 单位
└── 行(Row):实际数据记录
为什么要有「区」这一层?顺序 I/O 比随机 I/O 快得多。InnoDB 在分配新页时,尽量分配整个区(1MB 连续空间),让相邻数据在磁盘上物理相邻,提升顺序读性能。当表数据量很小时(< 1 个区),会使用碎片页逐页分配以节省空间。
1.6 InnoDB 数据页物理结构
每个数据页(默认 16KB)内部结构:
┌────────────────────────────────┐
│ File Header (38B) │ ← 页号、前后页指针、LSN
├────────────────────────────────┤
│ Page Header (56B) │ ← 页内记录数、空闲空间指针
├────────────────────────────────┤
│ Infimum + Supremum │ ← 最小/最大虚拟记录
├────────────────────────────────┤
│ User Records │ ← 实际行数据(按主键顺序单链表)
├────────────────────────────────┤
│ Free Space │ ← 未使用空间
├────────────────────────────────┤
│ Page Directory │ ← 槽(slot)数组,用于页内二分查找
├────────────────────────────────┤
│ File Trailer (8B) │ ← 校验和,保证页写入完整性
└────────────────────────────────┘
页内查找过程:先通过 Page Directory 中的槽做二分查找,定位到某个分组,再在该分组内顺序遍历(每组最多 8 条记录),快速找到目标行。
二、B+ 树索引体系
2.1 为什么选择 B+ 树
B+ 树相比 B 树的核心优势在于:所有数据都存储在叶子节点,非叶子节点仅存储键值和指针,使得每个非叶子节点可以容纳更多键值,树高更矮(通常 3~4 层即可支撑千万级数据);叶子节点通过双向链表相连,天然支持范围查询和顺序扫描。
容量估算:假设主键为 bigint(8B),指针为 6B,则非叶子节点每页可容纳 16KB / 14B ≈ 1170 个键值。叶子节点假设每行 1KB,则每页约 16 条记录。三层 B+ 树可存储 1170 × 1170 × 16 ≈ 2190 万行。
2.2 聚簇索引与二级索引
聚簇索引(主键索引):叶子节点存储完整行数据,表数据本身就是按聚簇索引组织的。每张 InnoDB 表有且仅有一个聚簇索引。如果没有显式定义主键,InnoDB 会选择第一个非空唯一索引,如果也没有则自动生成一个隐藏的 6 字节 row_id。
二级索引(辅助索引):叶子节点存储索引列值 + 对应的主键值。通过二级索引查询时,先在二级索引树中找到主键值,再回到聚簇索引树中通过主键查找完整行数据,这个过程称为「回表」。
2.3 覆盖索引
如果一个查询所需的所有列都包含在某个索引中,就可以直接从该索引获取数据而无需回表,称为覆盖索引。在 EXPLAIN 中表现为 Extra: Using index。
sql
-- 联合索引 (name, age)
SELECT name, age FROM user WHERE name = '张三'; -- 覆盖索引,无需回表
SELECT name, age, email FROM user WHERE name = '张三'; -- 需要回表取 email
2.4 联合索引与最左前缀原则
联合索引 (a, b, c) 在 B+ 树中按照 a → b → c 的顺序逐层排序。
最左前缀匹配规则 :查询条件必须从联合索引的最左列开始连续匹配。WHERE a=1 AND b=2 可以使用索引,WHERE b=2 无法使用该索引(因为在 a 未确定时,b 并不是全局有序的)。
范围查询中断 :WHERE a=1 AND b>5 AND c=3 中,a 和 b 可以使用索引,但 b 是范围查询后 c 无法继续利用索引排序(在 b 取值范围内,c 并不有序)。
2.5 索引下推(ICP)
MySQL 5.6 引入的 Index Condition Pushdown 优化。当使用联合索引 (a, b, c) 且查询条件含 a = 1 AND b > 5 AND c = 3 时:
- 无 ICP :对满足
a=1 AND b>5的每条记录都回表,取出完整行后再判断c=3。 - 有 ICP :在索引层直接判断
c=3(虽然 c 无法用于索引范围定位,但索引中存储了 c 的值),过滤掉不满足条件的记录后再回表,大幅减少回表次数。
EXPLAIN 中表现为 Extra: Using index condition。
2.6 B+ 树候选项淘汰推导
为什么不用其他数据结构:
- 哈希表:等值查询 O(1) 极快,但不支持范围查询、不支持排序、存在哈希冲突。
- 二叉搜索树/AVL 树:树高 O(logN),千万数据需 ~23 层,每层一次磁盘 IO 代价太高。
- 红黑树:同样树高过高的问题。
- B 树:非叶子节点也存数据,导致每个节点能容纳的键值更少,树更高;且范围查询需要中序遍历回溯父节点。
- 跳表:多级有序链表,范围查询友好但每级都是链表随机 IO,且空间利用率不如 B+ 树。
B+ 树在磁盘 IO 代价(树高矮、节点大小适配磁盘页)和范围查询性能之间取得了最优平衡。
2.7 页分裂与页合并
页分裂:当插入数据导致某页空间不足(默认达到 15/16 满时触发),InnoDB 新建一页,将原页约一半的数据移过去。代价是一次额外的磁盘写入,并产生空间碎片。
页合并:当删除数据导致某页数据量低于阈值(默认 1/2),InnoDB 尝试将相邻页合并为一页,回收空间。
主键选择对页分裂的影响:
自增整数主键:新数据永远追加到链表末尾,不触发中间页分裂
→ 写入性能好,碎片少
UUID/随机主键:新数据可能插入到链表中间任意位置
→ 频繁触发页分裂,大量随机 I/O,碎片多
→ 建议大量随机插入或删除后执行 OPTIMIZE TABLE 重建索引
2.8 索引失效的本质原因
B+ 树按 key 有序排列,查找依赖这种有序性。任何破坏有序性的操作都会导致索引失效,不需要死记硬背规则,从这个原理出发都能推导:
sql
-- 1. 对索引列做函数运算 → key 值被改变,有序性被破坏
WHERE YEAR(create_time) = 2024 -- ❌ 索引失效
WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01' -- ✅
-- 2. 隐式类型转换 → 本质是对索引列做了 CAST 函数
-- phone 是 varchar,传入数字时 MySQL 将 phone 列转为数字再比较
WHERE phone = 13800138000 -- ❌ 相当于 WHERE CAST(phone AS SIGNED) = 13800138000
WHERE phone = '13800138000' -- ✅
-- 3. 联合索引不满足最左前缀 → 跳过前导列后,后续列在全局并不有序
-- 联合索引 (a, b, c) 的 B+ 树排列:先按 a,a 相同按 b,b 相同按 c
WHERE b = 1 AND c = 2 -- ❌ 跳过 a,b 在全局分散无法定位
WHERE a = 1 AND c = 2 -- ⚠️ a 能用索引定位,c 无法利用(中间跳过了 b)
WHERE a = 1 AND b = 2 -- ✅
-- 4. LIKE 前缀通配符 → 起始位置不确定,无法在有序树中定位
WHERE name LIKE '%Mike' -- ❌
WHERE name LIKE 'Mike%' -- ✅ 前缀确定,可定位起始位置
-- 5. OR 条件中有非索引列 → 需要全表扫描兜底
WHERE a = 1 OR b = 2 -- 如果 b 无索引,整条语句走全表扫描
三、MVCC 多版本并发控制
3.1 核心组件
MVCC 让读操作不阻塞写操作、写操作不阻塞读操作,是 InnoDB 在 RC 和 RR 隔离级别下实现高并发的核心机制。它由三个核心组件协同工作:
隐藏列 :每行记录都有 trx_id(最后修改该行的事务 ID)和 roll_pointer(指向 undo log 中该行上一版本的指针)。
Undo Log 版本链:每次更新一行时,旧版本被写入 undo log,通过 roll_pointer 串成链表。版本链从最新版本开始,沿指针可回溯到任意历史版本。
ReadView:事务执行快照读时生成的"一致性视图",包含以下关键字段:
| 字段 | 含义 |
|---|---|
| m_ids | 生成 ReadView 时所有活跃(未提交)事务的 ID 列表 |
| min_trx_id | m_ids 中的最小值 |
| max_trx_id | 系统即将分配的下一个事务 ID(当前最大 trx_id + 1) |
| creator_trx_id | 生成该 ReadView 的事务自身的 ID |
3.2 可见性判断算法
对版本链中某个版本(trx_id = X)的判断逻辑:
1. X == creator_trx_id → 可见(自己修改的)
2. X < min_trx_id → 可见(在 ReadView 创建前已提交)
3. X >= max_trx_id → 不可见(在 ReadView 创建后才开始的事务)
4. min_trx_id <= X < max_trx_id:
- X 在 m_ids 中 → 不可见(该事务在创建 ReadView 时还未提交)
- X 不在 m_ids 中 → 可见(该事务在创建 ReadView 前已提交)
如果当前版本不可见,沿 roll_pointer 回溯到上一版本重复判断,直到找到可见版本或到达链表末尾。
3.3 RC 与 RR 的 ReadView 差异
- RC(读已提交):每次 SELECT 都生成新的 ReadView,因此能看到其他事务在本次 SELECT 之前提交的修改。
- RR(可重复读):仅在事务中第一次 SELECT 时生成 ReadView,后续 SELECT 复用同一个 ReadView,因此整个事务内看到的数据快照一致。
3.4 执行示例详解
初始状态:account 表有一行 (id=1, balance=100),由已提交事务 trx_id=1 写入。
时间线:
T1: 事务A (trx_id=10) BEGIN
T2: 事务B (trx_id=11) BEGIN
T3: 事务B 执行 UPDATE account SET balance=200 WHERE id=1
T4: 事务A 执行 SELECT balance FROM account WHERE id=1 ← 快照读
T5: 事务B COMMIT
T6: 事务A 执行 SELECT balance FROM account WHERE id=1 ← 快照读
T4 时刻分析(RR 隔离级别):
事务A 第一次 SELECT,生成 ReadView:m_ids=[10,11], min_trx_id=10, max_trx_id=12, creator_trx_id=10。
版本链当前头部:balance=200, trx_id=11。判断:11 在 m_ids 中 → 不可见。回溯到上一版本 balance=100, trx_id=1。判断:1 < min_trx_id(10) → 可见。结果:读到 balance=100。
T6 时刻分析:
RR 级别复用 T4 的 ReadView。虽然事务B已提交,但 ReadView 不变,仍然看到 balance=100。如果是 RC 级别,T6 会生成新的 ReadView:m_ids=[10], min_trx_id=10, max_trx_id=12,此时 trx_id=11 不在 m_ids 中 → 可见,读到 balance=200。
3.5 快照读与当前读
- 快照读:普通 SELECT 语句,通过 MVCC 读取历史版本,不加锁。
- 当前读 :
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT/UPDATE/DELETE,读取最新已提交版本并加锁。
混用场景陷阱:
sql
-- 事务A (RR级别)
BEGIN;
SELECT balance FROM account WHERE id=1; -- 快照读: 100
-- 此时事务B将balance改为200并提交
SELECT balance FROM account WHERE id=1 FOR UPDATE; -- 当前读: 200(最新值)
SELECT balance FROM account WHERE id=1; -- 快照读: 仍然100
快照读和当前读使用不同的可见性机制,在同一事务中混用可能导致"同一行读出不同值"的困惑。业务代码中应避免在同一事务内对同一数据交替使用两种读取方式。
四、锁机制
4.1 锁分类总览
InnoDB 的锁可以从多个维度分类:
按粒度:表级锁(意向锁、MDL 锁、AUTO-INC 锁)、行级锁(Record Lock、Gap Lock、Next-Key Lock)。
按模式:共享锁(S Lock,读锁)、排他锁(X Lock,写锁)。
4.2 意向锁
意向锁是表级锁,用于快速判断表中是否有行锁存在,避免加表锁时逐行扫描。
- IS(意向共享锁):事务准备对某行加 S 锁前,先在表上加 IS 锁。
- IX(意向排他锁):事务准备对某行加 X 锁前,先在表上加 IX 锁。
兼容矩阵:
| IS | IX | S | X | |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| S | ✅ | ❌ | ✅ | ❌ |
| X | ❌ | ❌ | ❌ | ❌ |
关键点:IS 与 IX 之间互相兼容(因为它们只是"意向",真正冲突在行级解决);意向锁只与表级 S/X 锁冲突。
4.3 Record Lock(记录锁)
锁定索引记录本身。SELECT * FROM t WHERE id = 5 FOR UPDATE 会对 id=5 这条索引记录加 X 型 Record Lock。
4.4 Gap Lock(间隙锁)
锁定索引记录之间的间隙,防止其他事务在间隙中插入新记录,是 RR 级别下解决幻读的关键机制。
Gap Lock 之间不互斥(即使是 X 型的 Gap Lock 彼此也兼容),它们只阻塞 INSERT 操作。
索引记录: [3, 7, 11, 15]
Gap Lock on (3,7): 阻止在 id=4,5,6 的位置插入
Gap Lock on (7,11): 阻止在 id=8,9,10 的位置插入
4.5 Next-Key Lock
Next-Key Lock = Record Lock + Gap Lock,是一个左开右闭的区间锁 (a, b]。InnoDB 在 RR 级别下默认对扫描到的索引记录加 Next-Key Lock。
加锁规则(重要):
- 加锁的基本单位是 Next-Key Lock。
- 查找过程中访问到的对象才会加锁。
- 唯一索引上的等值查询,命中记录时 Next-Key Lock 退化为 Record Lock。
- 唯一索引上的等值查询,未命中记录时 Next-Key Lock 退化为 Gap Lock。
- 非唯一索引上的等值查询,向右遍历到第一个不满足条件的记录时,Next-Key Lock 退化为 Gap Lock。
4.6 MDL 锁(元数据锁)
MDL 锁保护表结构不被并发修改。执行 DML 语句时自动加 MDL 读锁,执行 DDL 语句时自动加 MDL 写锁。MDL 读锁之间不冲突,但 MDL 写锁与任何 MDL 锁冲突。
经典阻塞场景:
DB 后续查询 DDL(ALTER TABLE) 长事务(SELECT) DB 后续查询 DDL(ALTER TABLE) 长事务(SELECT) #mermaid-svg-4GqxVd1B0FUFvdrP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4GqxVd1B0FUFvdrP .error-icon{fill:#552222;}#mermaid-svg-4GqxVd1B0FUFvdrP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4GqxVd1B0FUFvdrP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4GqxVd1B0FUFvdrP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4GqxVd1B0FUFvdrP .marker.cross{stroke:#333333;}#mermaid-svg-4GqxVd1B0FUFvdrP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4GqxVd1B0FUFvdrP p{margin:0;}#mermaid-svg-4GqxVd1B0FUFvdrP .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4GqxVd1B0FUFvdrP text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4GqxVd1B0FUFvdrP .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4GqxVd1B0FUFvdrP .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4GqxVd1B0FUFvdrP .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4GqxVd1B0FUFvdrP .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4GqxVd1B0FUFvdrP #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4GqxVd1B0FUFvdrP .sequenceNumber{fill:white;}#mermaid-svg-4GqxVd1B0FUFvdrP #sequencenumber{fill:#333;}#mermaid-svg-4GqxVd1B0FUFvdrP #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4GqxVd1B0FUFvdrP .messageText{fill:#333;stroke:none;}#mermaid-svg-4GqxVd1B0FUFvdrP .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4GqxVd1B0FUFvdrP .labelText,#mermaid-svg-4GqxVd1B0FUFvdrP .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4GqxVd1B0FUFvdrP .loopText,#mermaid-svg-4GqxVd1B0FUFvdrP .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4GqxVd1B0FUFvdrP .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4GqxVd1B0FUFvdrP .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4GqxVd1B0FUFvdrP .noteText,#mermaid-svg-4GqxVd1B0FUFvdrP .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4GqxVd1B0FUFvdrP .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4GqxVd1B0FUFvdrP .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4GqxVd1B0FUFvdrP .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4GqxVd1B0FUFvdrP .actorPopupMenu{position:absolute;}#mermaid-svg-4GqxVd1B0FUFvdrP .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4GqxVd1B0FUFvdrP .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4GqxVd1B0FUFvdrP .actor-man circle,#mermaid-svg-4GqxVd1B0FUFvdrP line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4GqxVd1B0FUFvdrP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一个长事务 + 一个 DDL = 后续所有查询全部阻塞 获取 MDL 读锁(长时间持有) 申请 MDL 写锁 → 被 A 阻塞 申请 MDL 读锁 → 被 B 阻塞(写锁优先级高)
防范措施 :DDL 前先检查是否有长事务(information_schema.innodb_trx),使用 ALTER TABLE ... , ALGORITHM=INPLACE, LOCK=NONE 或设置 lock_wait_timeout 避免长时间等待。
4.7 AUTO-INC 锁
用于自增列的值分配。MySQL 5.1+ 通过 innodb_autoinc_lock_mode 控制:
- mode=0:传统模式,每次 INSERT 持有表级 AUTO-INC 锁直到语句结束。
- mode=1:连续模式(默认),简单 INSERT 使用轻量级互斥锁,批量 INSERT 仍使用表锁。
- mode=2:交错模式,所有 INSERT 都用轻量级锁,并发最高但自增值可能不连续。
五、三大日志
5.1 Redo Log(重做日志)
作用:保证事务持久性(Durability)。记录的是「物理变更」------即页面的具体修改内容(如"将第 X 页偏移 Y 处的值从 A 改为 B")。
WAL 机制:Write-Ahead Logging,先将修改写入 redo log buffer,再由后台刷到磁盘上的 redo log 文件(顺序写入),而数据页的随机写入可以延后。这将随机写转化为顺序写,极大提升性能。
写入时机 (innodb_flush_log_at_trx_commit):
- 0:每秒刷盘一次,宕机最多丢 1 秒数据。
- 1(默认):每次事务提交都刷盘,最安全但性能最低。
- 2:每次提交写入 OS Page Cache,每秒 fsync,OS 崩溃才丢数据。
循环写入:Redo log 由固定大小的文件组组成(如 4 个各 1GB 的文件),通过 write pos 和 checkpoint 两个指针形成环形缓冲。write pos 追赶 checkpoint 表示空间满了需要推进 checkpoint(刷脏页)。
5.2 Undo Log(回滚日志)
作用:保证事务原子性(Atomicity)+ 支撑 MVCC。记录的是「逆操作」,使得事务可以回滚。
- INSERT 操作记录:删除该行的逆操作。
- DELETE 操作记录:重新插入该行的逆操作。
- UPDATE 操作记录:将列值改回原值的逆操作。
Undo log 同时构成 MVCC 的版本链,通过 roll_pointer 串联历史版本供快照读使用。
回收机制:事务提交后 undo log 不会立即删除,需等到没有任何活跃事务可能访问到该版本后,由 purge 线程异步清理。长事务可能导致 undo log 膨胀。
5.3 Binlog(归档日志)
作用:MySQL Server 层的日志,用于主从复制和数据恢复(PITR)。记录的是「逻辑变更」------SQL 语句或行变更事件。
三种格式:
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | 原始 SQL 语句 | 日志量小,易读 | 非确定性函数(NOW()、RAND()、UUID())可能导致主从不一致 |
| ROW | 行变更前后的完整数据 | 精确还原,不受函数影响 | 日志量大(尤其大批量 UPDATE) |
| MIXED | 默认用 STATEMENT,不安全时自动切换 ROW | 兼顾体积和安全 | 判断逻辑复杂,特殊场景仍可能出问题 |
STATEMENT 不安全的典型场景:
sql
-- 含非确定性函数
INSERT INTO t VALUES (UUID(), NOW());
-- 含 LIMIT 的 DELETE/UPDATE(执行计划可能主从不同)
DELETE FROM t WHERE status=0 ORDER BY id LIMIT 100;
-- 使用用户自定义变量
SET @i=0; UPDATE t SET seq=(@i:=@i+1);
生产环境推荐使用 ROW 格式,配合 binlog_row_image=FULL 保证数据安全。
5.4 两阶段提交
为了保证 redo log 和 binlog 的一致性,MySQL 使用两阶段提交(2PC):
1. [Prepare 阶段] 写 redo log,标记为 prepare 状态
2. [Commit 阶段] 写 binlog → 将 redo log 标记为 commit 状态
崩溃恢复逻辑:
- redo log 为 prepare + binlog 完整 → 提交事务
- redo log 为 prepare + binlog 不完整 → 回滚事务
- redo log 没有 prepare 记录 → 回滚事务
5.5 三大日志协作流程
以 UPDATE account SET balance=200 WHERE id=1 为例:
1. 查找 id=1 的记录,不在 Buffer Pool 则从磁盘读入
2. 将旧值写入 undo log(支持回滚 + MVCC)
3. 在内存中修改 balance 为 200(产生脏页)
4. 写 redo log 到 redo log buffer(记录物理变更)
5. 事务提交:
5a. redo log 刷盘,标记 prepare
5b. binlog 刷盘
5c. redo log 标记 commit
6. 后台线程择机将脏页刷回磁盘(checkpoint)
六、事务与隔离级别
6.1 ACID 特性
- 原子性(Atomicity):由 undo log 保证,事务要么全部成功,要么全部回滚。
- 一致性(Consistency):由应用逻辑 + 数据库约束共同保证,是 ACID 的最终目标。
- 隔离性(Isolation):由锁 + MVCC 保证,并发事务互不干扰。
- 持久性(Durability):由 redo log 保证,已提交的数据不会因宕机丢失。
6.2 四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✅可能 | ✅可能 | ✅可能 |
| READ COMMITTED (RC) | ❌不会 | ✅可能 | ✅可能 |
| REPEATABLE READ (RR) | ❌不会 | ❌不会 | ❌基本不会* |
| SERIALIZABLE | ❌不会 | ❌不会 | ❌不会 |
*InnoDB 在 RR 级别下通过 MVCC + Next-Key Lock 基本解决了幻读问题,但在特定的快照读+当前读混用场景下仍可能出现。
6.3 秒杀场景串联事务全貌
一个秒杀扣减库存的事务,完整涉及 ACID + 锁 + MVCC + 日志的协作:
sql
BEGIN;
-- 1. 当前读 + 加 X 锁(防止超卖)
SELECT stock FROM goods WHERE id=1001 FOR UPDATE;
-- 2. 应用层判断 stock > 0
-- 3. 扣减库存
UPDATE goods SET stock = stock - 1 WHERE id=1001;
-- 4. 写入订单
INSERT INTO orders (user_id, goods_id) VALUES (123, 1001);
COMMIT;
执行过程拆解:
- SELECT FOR UPDATE:当前读,对 id=1001 加 X 型 Record Lock,其他事务的 FOR UPDATE 会阻塞等待。
- UPDATE:写 undo log(旧 stock 值),修改内存中的数据页,写 redo log。
- INSERT:写 undo log(insert 类型),插入行,写 redo log。
- COMMIT:两阶段提交(redo prepare → binlog flush → redo commit),释放所有行锁。
- MVCC:并发的普通 SELECT(快照读)不会被阻塞,读到的是事务开始前的 stock 值。
- Crash Recovery:如果提交前崩溃,通过 undo log 回滚;如果提交后崩溃,通过 redo log 重做。
七、死锁
7.1 死锁产生条件
死锁的四个必要条件:互斥(锁资源独占)、持有并等待(持有锁的同时请求其他锁)、不可抢占(已持有的锁不能被强制释放)、循环等待(事务间形成等待环路)。
7.2 典型死锁场景
场景一:交叉加锁(最经典)
sql
-- 事务A -- 事务B
UPDATE t SET v=1 WHERE id=1; -- 锁住 id=1
UPDATE t SET v=2 WHERE id=2; -- 锁住 id=2
UPDATE t SET v=1 WHERE id=2; -- 等待 id=2 的锁
UPDATE t SET v=2 WHERE id=1; -- 等待 id=1 的锁 → 死锁
场景二:Gap Lock 冲突
sql
-- 索引记录: [5, 10, 15]
-- 事务A -- 事务B
SELECT * FROM t WHERE id=8 FOR UPDATE; -- 加 Gap Lock (5,10)
SELECT * FROM t WHERE id=7 FOR UPDATE; -- 也加 Gap Lock (5,10),兼容
INSERT INTO t VALUES (8, ...); -- 被事务B的 Gap Lock 阻塞
INSERT INTO t VALUES (7, ...); -- 被事务A的 Gap Lock 阻塞 → 死锁
Gap Lock 之间兼容但都阻塞 INSERT,当两个事务各自持有 Gap Lock 后互相要在对方的间隙中 INSERT 时产生死锁。
场景三:唯一索引冲突(INSERT 导致)
sql
-- 事务A -- 事务B -- 事务C
INSERT INTO t(id) VALUES(10);
INSERT INTO t(id) VALUES(10); -- 唯一冲突,等待 S 锁
INSERT INTO t(id) VALUES(10); -- 唯一冲突,等待 S 锁
ROLLBACK; -- A 回滚释放锁
-- B、C 同时获得 S 锁,都尝试加 X 锁插入 → 死锁
场景四:FOR UPDATE 范围不一致
sql
-- 事务A -- 事务B
SELECT * FROM t WHERE id BETWEEN 1 AND 5 SELECT * FROM t WHERE id BETWEEN 3 AND 8
FOR UPDATE; -- 锁 [1,5] FOR UPDATE; -- 锁 [3,8]
-- A 持有 1~5 的锁,等待 6~8 -- B 持有 6~8 的锁,等待 1~2
-- 交叉区间 [3,5] 产生循环等待 → 死锁
场景五:索引失效导致锁扩大
sql
-- 表 t 有索引 idx_status,但 status 只有 0/1 两个值(选择性极低)
-- 事务A -- 事务B
UPDATE t SET x=1 WHERE status=0 AND city='BJ'; -- 索引失效,实际全表扫描加锁
UPDATE t SET x=2 WHERE status=1 AND city='SH'; -- 同样全表扫描
-- 两个事务本意操作不同行,但因索引失效都锁了全表 → 死锁
7.3 死锁处理
InnoDB 默认开启死锁检测(innodb_deadlock_detect=ON),使用 wait-for graph 检测环路,一旦发现死锁立即选择回滚代价最小的事务(通常是持有锁最少/undo log 最少的事务)。
预防策略:固定加锁顺序、缩小事务范围、使用合理索引避免锁升级、RC 级别可减少 Gap Lock 相关死锁。
八、慢查询诊断
8.1 慢查询分类与优化
第一类:全表扫描(未命中索引)
sql
-- 问题:隐式类型转换导致索引失效
SELECT * FROM orders WHERE order_no = 123456; -- order_no 是 varchar,传入 int
-- 优化:
SELECT * FROM orders WHERE order_no = '123456';
-- 问题:函数操作导致索引失效
SELECT * FROM users WHERE DATE(create_time) = '2024-01-01';
-- 优化:
SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
-- 问题:前模糊匹配
SELECT * FROM users WHERE name LIKE '%张';
-- 优化方案:使用覆盖索引 + 改写查询逻辑,或引入全文索引/ES
第二类:回表过多
sql
-- 问题:SELECT * 导致大量回表
SELECT * FROM orders WHERE status = 1 AND create_time > '2024-01-01';
-- 优化:只查需要的字段 + 建立覆盖索引
SELECT order_id, amount FROM orders WHERE status = 1 AND create_time > '2024-01-01';
-- 创建联合索引 (status, create_time, order_id, amount)
第三类:排序和临时表
sql
-- 问题:filesort
SELECT * FROM orders WHERE user_id = 100 ORDER BY create_time;
-- 优化:联合索引 (user_id, create_time) 利用索引有序性避免排序
-- 问题:GROUP BY 产生临时表
SELECT city, COUNT(*) FROM users GROUP BY city;
-- 优化:创建 city 索引,使得分组可以利用索引顺序
第四类:深度分页
sql
-- 问题:OFFSET 很大时需要扫描大量行后丢弃
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;
-- 优化方案1:延迟关联
SELECT * FROM orders a
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) b ON a.id = b.id;
-- 优化方案2:游标分页(记住上次最大 ID)
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;
第五类:不合理的 JOIN
sql
-- 问题:驱动表选择错误 + 被驱动表无索引
SELECT * FROM big_table a LEFT JOIN small_table b ON a.code = b.code;
-- 优化:小表驱动大表 + 确保被驱动表关联字段有索引
SELECT * FROM small_table a LEFT JOIN big_table b ON a.code = b.code;
-- 在 big_table.code 上建立索引
8.2 EXPLAIN 关键字段
- type(由好到差):system > const > eq_ref > ref > range > index > ALL
- key:实际使用的索引
- rows:预估扫描行数
- Extra:Using index(覆盖索引)、Using index condition(ICP)、Using filesort(需要排序)、Using temporary(使用临时表)
九、分布式 MySQL
9.1 三类分布式需求
不同场景对"分布式"的诉求不同:
- 高可用:主从复制 + 故障自动切换 → MySQL Replication + MHA/Orchestrator 或 InnoDB Cluster
- 读扩展:读多写少场景增加只读副本分担读压力 → 一主多从 + 读写分离中间件
- 写扩展 + 大容量:单机存储或写入瓶颈 → 分库分表(Sharding)
9.2 主从复制原理
Master Slave
┌──────────┐ ┌──────────┐
│ binlog │ ──dump thread──→ │ relay log│
└──────────┘ └──────────┘
│
SQL thread → 回放到从库数据
复制模式演进:异步复制(延迟不可控)→ 半同步复制(至少一个从库收到 relay log 后 master 才返回)→ 组复制 / MGR(基于 Paxos 的多主方案)。
9.3 InnoDB Cluster
MySQL 官方的高可用方案,由三个组件构成:
- Group Replication(MGR):基于 Paxos 协议的多副本同步,提供强一致性保证。
- MySQL Shell:管理和部署工具。
- MySQL Router:透明路由层,应用无需感知主从切换。
典型部署为三节点(一主两从),主节点故障后自动选举新主,RTO 通常在秒级。
9.4 NDB Cluster
MySQL NDB Cluster 是一种基于共享无存储(Shared-Nothing)架构的分布式方案,将数据分片存储在内存中的 NDB 存储引擎节点上。适合需要极低延迟和自动分片的电信级场景,但与常规 InnoDB 生态兼容性有限,生产中较少使用。
9.5 生产选型参考
- 中小规模高可用:InnoDB Cluster / MHA + ProxySQL
- 读扩展:一主多从 + MaxScale/ProxySQL 读写分离
- 分库分表:ShardingSphere / Vitess,或业务层自行路由
- NewSQL 替代:TiDB(兼容 MySQL 协议,自动分片+分布式事务)、OceanBase(蚂蚁自研,金融级强一致)
十、分库分表与平滑迁移
10.1 分片策略
- Hash 取模 :
shard_id = hash(sharding_key) % N,数据分布均匀但扩容需 rehash。 - Range 分片:按范围划分(如按 user_id 0~100W 分到库1),易于扩容但可能热点不均。
- 一致性 Hash:减少扩容时数据迁移量。
10.2 分片后的挑战
跨片查询、分布式事务、全局唯一 ID 生成(雪花算法等)、跨片 JOIN 和聚合、分片键选择(应覆盖绝大多数查询条件)。
10.3 平滑迁移方案
大型系统从单表迁移到分表,需要保证迁移过程中业务零停机、数据零丢失。
双写 + 灰度切读方案:
阶段1:历史数据迁移(全量同步)
↓
阶段2:开启双写(单表 + 分表同时写入)+ 增量数据追平
↓
阶段3:数据一致性校验
↓
阶段4:灰度切读(逐步将读流量切到分表)
↓
阶段5:全量切读 → 停止双写 → 下线旧表
双写核心逻辑(Java 伪代码):
java
@Service
public class OrderService {
@Autowired private OldOrderDao oldDao;
@Autowired private ShardOrderDao shardDao;
@Autowired private MigrationConfig config;
public void createOrder(Order order) {
// 主库写入(必须成功)
oldDao.insert(order);
// 分表写入(异步 + 容错)
if (config.isDoubleWriteEnabled()) {
try {
shardDao.insert(order);
} catch (Exception e) {
// 写入失败放入补偿队列,后续重试
compensationQueue.push(order);
log.warn("分表写入失败,已放入补偿队列", e);
}
}
}
public Order queryOrder(String orderId) {
// 灰度切读
if (config.shouldReadFromShard(orderId)) {
return shardDao.findById(orderId);
}
return oldDao.findById(orderId);
}
}
数据一致性校验:定时任务对比新旧表数据,以旧表为基准修复分表中的差异。校验通过率达到 100% 后才能推进灰度切读比例。
十一、字段类型选择最佳实践
11.1 字符串类型
| 类型 | 存储方式 | 适用场景 | 注意事项 |
|---|---|---|---|
| CHAR(N) | 固定长度,不足补空格 | 长度固定的编码(MD5、UUID去横杠、身份证号) | 最大 255 字符,读取时自动去尾部空格 |
| VARCHAR(N) | 可变长度 + 1~2 字节长度前缀 | 大多数字符串场景 | N 建议按业务实际最大长度定义,不要无脑 VARCHAR(255) |
| TEXT | 独立存储(行溢出) | 长文本(文章内容、JSON 原始数据) | 不能有默认值,查询效率低,建议拆到独立表 |
VARCHAR(N) 的 N 该设多大?N 是字符数而非字节数(UTF8MB4 下一个字符最多 4 字节)。设置为业务实际最大长度即可,不是"越大越好"。虽然 VARCHAR 按实际长度存储不浪费磁盘,但 N 过大会影响:内存临时表大小估算(Memory 引擎按最大长度分配)、InnoDB 行溢出判断、排序时 sort_buffer 分配。
11.2 数值类型
| 类型 | 字节 | 范围 | 适用场景 |
|---|---|---|---|
| TINYINT | 1 | -128~127 / 0~255 | 状态码、标志位 |
| INT | 4 | ±21亿 | 大部分整数 |
| BIGINT | 8 | ±9.2×10^18 | 自增主键、雪花 ID |
| DECIMAL(M,D) | 变长 | 精确小数 | 金额(避免浮点精度丢失) |
DECIMAL vs FLOAT/DOUBLE :金融、电商金额必须用 DECIMAL,因为 FLOAT/DOUBLE 是近似存储,0.1 + 0.2 ≠ 0.3。如果存储的金额精度固定(如精确到分),也可以用 BIGINT 存分值(如 100 表示 1.00 元),应用层做转换。
11.3 时间类型
| 类型 | 字节 | 范围 | 精度 | 适用场景 |
|---|---|---|---|---|
| DATETIME | 8 | 1000-9999年 | 微秒 | 业务时间(不受时区影响) |
| TIMESTAMP | 4 | 1970-2038年 | 秒 | 记录时间戳(自动转 UTC 存储) |
| BIGINT | 8 | 无限制 | 毫秒 | 跨系统交互(Java timestamp) |
推荐 :业务字段用 DATETIME(范围更大、不受 2038 问题影响);create_time/update_time 可用 TIMESTAMP 配合 DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 自动填充;需要跨时区的系统可统一使用 BIGINT 存 Unix 毫秒时间戳。
11.4 布尔/状态类型
MySQL 没有真正的布尔类型,BOOLEAN 等价于 TINYINT(1)。
- 二值状态(是/否、启用/禁用):使用
TINYINT(1)+ 约束CHECK (col IN (0, 1))或应用层校验。 - 多值状态(订单状态等):使用
TINYINT或SMALLINT+ 枚举值注释。 - 不建议使用 ENUM:虽然节省空间,但修改枚举值需要 ALTER TABLE,且排序按内部编号而非字面值。
十二、高并发数据库设计分层
12.1 连接层优化
- 使用连接池(Druid/HikariCP)避免频繁创建销毁连接。
- 合理设置
max_connections和连接池大小(通常 CPU 核心数 × 2 + 磁盘数)。 - 短连接场景注意 TIME_WAIT 堆积。
12.2 缓存层
- 热点数据前置 Redis 缓存,减少数据库压力。
- 缓存策略:Cache-Aside(旁路缓存)为主,注意缓存穿透/击穿/雪崩的防护。
- 数据一致性:延迟双删、binlog 异步更新缓存。
12.3 SQL 优化层
- 只查需要的字段,避免
SELECT *。 - 合理使用索引,关注 EXPLAIN 中的 type 和 rows。
- 批量操作代替循环单条操作。
- 避免在事务中进行 RPC 调用或耗时操作。
12.4 架构层
- 读写分离分担负载。
- 分库分表应对数据量和写入瓶颈。
- 异步化:非核心链路走消息队列异步处理。
总结
MySQL 的核心原理可以串联为一条主线:
数据存储 (B+ 树索引 + InnoDB 页结构)→ 并发控制 (MVCC 快照读 + 锁机制保护当前读)→ 数据安全 (redo log 保证持久性 + undo log 保证原子性 + binlog 保证可恢复性 + 两阶段提交保证一致性)→ 性能优化 (Buffer Pool 加速读写 + Change Buffer 优化非唯一索引写入 + 覆盖索引减少回表 + ICP 减少无效回表)→ 横向扩展(主从复制实现高可用和读扩展 + 分库分表解决容量瓶颈)。
理解这条主线,再深入每个模块的细节,就能建立起完整的 MySQL 知识体系。