MySQL 崩溃以后怎么恢复数据?redo log、undo log 与 Crash Recovery

如果 MySQL 突然断电或者进程崩溃,Buffer Pool 中的数据全没了,InnoDB 到底是怎么恢复数据的?

例如服务器宕机前可能是:

text 复制代码
事务 A:

已经 COMMIT
但是部分 Dirty Page 还没有刷盘


事务 B:

还没有 COMMIT
但是部分修改过的 Page
可能已经被刷到了磁盘

突然:

text 复制代码
 MySQL 崩溃

这时候就同时存在两个问题:

text 复制代码
事务 A:
该保留的修改可能还没写入数据文件

事务 B:
不该保留的修改却可能已经写入数据文件

InnoDB 怎么处理?

答案就是:

先利用 redo log 恢复应该存在的数据页修改,再利用 undo log 回滚没有提交完成的事务。

这就是 Crash Recovery 的核心思想。


1. 什么是 Crash Recovery?

Crash Recovery 就是:

MySQL 在发生异常宕机之后,重新启动时自动把 InnoDB 恢复到一致状态的过程。

例如:

  • 操作系统突然崩溃
  • 服务器断电
  • mysqld 进程被强制终止
  • 机器突然重启

都可能导致:

text 复制代码
Buffer Pool
中的内容直接丢失

但是磁盘上的:

redo log

undo log

数据文件

等仍然存在。

InnoDB 就利用这些持久化信息完成恢复。

MySQL 官方也把事务、COMMIT、ROLLBACK 和 crash recovery 作为 InnoDB ACID 能力的重要组成部分。


2. 为什么崩溃以后不能直接使用磁盘数据?

因为:

事务提交成功,并不意味着所有 Dirty Page 都已经写回磁盘。

例如执行:

sql 复制代码
UPDATE account
SET balance = 900
WHERE id = 1;

事务已经:

COMMIT

但可能仍然是:

text 复制代码
Buffer Pool:

balance = 900


磁盘数据文件:

balance = 1000

因为内存中的 Page 是 Dirty Page。

只要对应 redo log 已经满足持久化要求,事务就不需要等待整个数据页刷盘。

如果此时突然宕机:

text 复制代码
Buffer Pool
↓
全部丢失

磁盘里却还是:

balance = 1000

所以如果 MySQL 重启以后直接相信数据文件:

已经提交的 900 就丢了。

这就是为什么需要 redo log。


3. redo log 在崩溃恢复中做什么?

redo log 记录的是数据页发生修改时所需要的重做信息。

它最大的作用之一就是:

恢复那些已经发生、但还没有完整写入数据文件的 Page 修改。

例如:

text 复制代码
磁盘:

balance = 1000


redo:

这个 Page 后来发生过修改
1000 → 900

MySQL 重启以后:

text 复制代码
读取 redo
    ↓
重新应用需要恢复的修改
    ↓
balance = 900

这就是 redo 这个名字的含义:

redo = 再做一次

MySQL 8.4 官方明确说明,redo log 是用于 crash recovery 的磁盘结构;异常关闭之前没有完成写入数据文件的修改,会在初始化时自动重新应用。


4. 那 undo log 又是干什么的?

现在再看另一种情况。

假设事务 B:

sql 复制代码
START TRANSACTION;

UPDATE account
SET balance = 500
WHERE id = 2;

但是还没有:

sql 复制代码
COMMIT;

服务器就崩了。

你可能会想:

没有 COMMIT,修改应该还只存在 Buffer Pool,宕机以后内存丢了,不就自动恢复了吗?

不一定。

因为 InnoDB 允许脏页在事务提交之前就刷入磁盘。

例如:

text 复制代码
事务 B:

balance = 1000
        ↓
修改
        ↓
balance = 500

但还没有 COMMIT

此时某个 Page 可能已经被后台刷盘:

text 复制代码
磁盘:

balance = 500

然后突然:

text 复制代码
💥 宕机

那么磁盘上已经出现了:

一个未提交事务的修改。

这时候就需要 undo log。


5. 所以 redo 和 undo 分别解决什么问题?

redo:

把应该存在的修改重新做出来。

undo:

把不应该存在的修改撤销掉。

例如:

text 复制代码
事务 A:

已经 COMMIT
但是 Page 没刷盘

↓
redo 恢复

而:

text 复制代码
事务 B:

没有 COMMIT
但是 Page 已经刷盘

↓
undo 回滚

所以整个恢复过程可以简单理解为:

text 复制代码
Crash
  ↓
redo
  ↓
把数据页恢复到崩溃时应该具有的状态
  ↓
undo
  ↓
回滚未完成事务
  ↓
得到一致的数据状态

这就是 redo + undo 的核心配合。


6. 例子

假设初始数据:

text 复制代码
A = 100
B = 100

现在有两个事务。

事务 T1:

sql 复制代码
UPDATE account
SET balance = 200
WHERE id = 1;

COMMIT;

事务 T1 已经提交。

但是:

text 复制代码
对应 Dirty Page
还没有刷盘

所以磁盘可能仍然:

text 复制代码
A = 100

与此同时事务 T2:

sql 复制代码
START TRANSACTION;

UPDATE account
SET balance = 300
WHERE id = 2;

但是 T2:

没有 COMMIT

恰好 T2 修改的数据页已经被后台刷盘。

于是宕机前:

text 复制代码
T1:

应该是 A = 200
但磁盘还是 A = 100


T2:

不应该保留 B = 300
但磁盘已经是 B = 300

突然:

text 复制代码
💥 Crash

磁盘现在可能是:

text 复制代码
A = 100
B = 300

但正确结果应该是:

text 复制代码
A = 200
B = 100

怎么办?


7. 第一步:先应用 redo

MySQL 重启以后,InnoDB 会进行 redo log application。

根据 redo,把需要恢复的 Page 修改重新应用。

于是:

text 复制代码
磁盘:

A = 100
B = 300

       ↓ redo

恢复相关 Page 修改

这里非常重要:

redo 并不是简单只恢复"已经提交事务"的修改。

redo 首先关注的是:

数据页曾经发生过哪些需要恢复的修改。

因此恢复后的状态可以先理解成接近:

text 复制代码
A = 200
B = 300

也就是说:

事务 T1 的修改恢复出来了。

事务 T2 的页面修改也可能存在。

这没问题。

因为:

下一步还有 undo。

官方的恢复流程也是先进行 redo log application,然后再处理未完成事务的回滚。


8. 第二步:回滚未完成事务

redo 应用以后,InnoDB 还需要检查:

崩溃发生时,有哪些事务没有正常完成?

例如:

text 复制代码
T1:

COMMIT

应该保留。

而:

text 复制代码
T2:

ACTIVE
没有 COMMIT

就不能保留。

所以 InnoDB 根据 undo log:

text 复制代码
B = 300
    ↓
undo
    ↓
B = 100

最终:

text 复制代码
A = 200
B = 100

数据库重新恢复到一致状态。

MySQL 官方明确说明,InnoDB 会自动回滚崩溃时存在的未提交事务。


9. 为什么不直接只 redo 已提交事务?

你可能会想:

既然最终只需要已提交事务,为什么 redo 阶段不直接判断这个 redo 属于已提交还是未提交?

因为 redo 的设计重点是:

Page 级别的物理恢复。

redo 更关心的是:

text 复制代码
某个 Page
发生过哪些修改

而事务最终:

text 复制代码
提交
还是
回滚

属于更高层次的事务逻辑。

所以 InnoDB 把两件事分开:

text 复制代码
redo
↓
恢复 Page 修改

undo
↓
处理事务回滚

这样职责非常清晰。

可以记成:

text 复制代码
redo:

先恢复"物理现场"


undo:

再清理"不应该留下的事务修改"

10. 为什么 redo 要先执行?

假设未提交事务 T2 修改了某个 Page。

这个 Page:

text 复制代码
根本还没有完整写到磁盘

但是 undo 自己需要的数据结构也属于 InnoDB 页面体系的一部分。

如果 Page 状态都还没有先恢复好:

InnoDB 可能连完整的事务恢复基础都没有。

所以从整体理解上:

text 复制代码
先通过 redo
恢复必要的数据页状态
        ↓
再根据事务信息和 undo
回滚未完成事务

更加合理。

这也就是为什么 Crash Recovery 不是简单的:

text 复制代码
找到没提交事务
↓
直接 undo

而是需要先进行 redo recovery。


11. redo 从哪里开始恢复?

假设 MySQL 已经运行了:

30 天

redo log 中经历了海量修改。

崩溃以后难道要:

从数据库创建的第一天开始把所有 redo 重放一遍?

显然不可能。

所以 InnoDB 需要一个非常重要的机制:

Checkpoint


12. 什么是 Checkpoint?

Checkpoint 表示:在这个位置之前需要保证的旧修改,已经有足够的数据页状态持久化到磁盘,因此 Crash Recovery 不需要再从更早的位置重新做起。

InnoDB 会记录:

Checkpoint LSN

服务器崩溃以后:

text 复制代码
找到最近 Checkpoint
        ↓
从 Checkpoint 附近开始扫描 redo
        ↓
应用之后需要恢复的修改

而不是:

text 复制代码
从 redo 历史最开始的位置
一路恢复到最后

MySQL 官方明确说明,在 crash recovery 时,InnoDB 会寻找日志中的 checkpoint 标记,并从该 checkpoint 向前扫描 redo、应用之后的修改。


13. 什么是 LSN?

Log Sequence Number

可以简单理解成:

redo log 中不断向前增长的位置编号。

随着 redo 不断产生:

text 复制代码
LSN 100

LSN 200

LSN 300

LSN 400

LSN 500

不断向前增长。

例如:

text 复制代码
Checkpoint LSN = 300

Current LSN = 500

那么 crash recovery 大致关注:

text 复制代码
300
 ↓
400
 ↓
500

这个范围中的 redo。

MySQL 8.4 还提供了诸如 Innodb_redo_log_checkpoint_lsn、Innodb_redo_log_current_lsn、Innodb_redo_log_flushed_to_disk_lsn 等状态变量,用来表示 checkpoint、当前 redo 位置和已持久化位置。


14. Checkpoint 是把整个 Buffer Pool 一次性刷盘吗?

不是。

你可能会认为:

text 复制代码
Checkpoint
↓
暂停数据库
↓
把所有 Dirty Page 全刷盘
↓
继续运行

InnoDB 并不是这么做的。

它采用的是:

Fuzzy Checkpoint

可以理解成:

后台逐渐把 Dirty Page 分批刷盘,然后不断向前推进 Checkpoint。

官方明确说明,InnoDB 使用 fuzzy checkpoint,不需要一次性把整个 Buffer Pool 全部刷盘,而是以小批量方式持续 Flush 修改过的数据页。

所以更接近:

text 复制代码
Dirty Page
Dirty Page
Dirty Page
Dirty Page
     ↓
后台逐步 Flush
     ↓
Checkpoint LSN
逐渐向前推进

而不是:

text 复制代码
停止整个数据库
↓
一次性刷完全部脏页

15. 为什么 Checkpoint 很重要?

主要有两个原因。

第一:

减少 Crash Recovery 需要扫描和重做的 redo 范围。

例如没有 Checkpoint:

text 复制代码
redo:

1 → 1000000

崩溃以后可能需要处理大量日志。

有 Checkpoint:

text 复制代码
Checkpoint = 950000

Current = 1000000

就只需要重点处理:

text 复制代码
950000 → 1000000

恢复速度明显更合理。


第二:

允许旧 redo 空间被重新利用。

redo log 的空间不是无限增长的。

当某些旧 redo 已经对应到:

text 复制代码
相关 Dirty Page
已经安全刷盘

以后,这些旧日志就不再是 Crash Recovery 必需的信息。

因此对应日志空间可以被后续 redo 重用。

MySQL 的 redo 实现文档也说明,last_checkpoint_lsn 之前的 redo 已经不再需要用于恢复,可以被覆盖重用。


16. WAL、Checkpoint、Crash Recovery 是什么关系?

首先 WAL 保证:

对应 Dirty Page 刷入数据文件之前,相关 redo 必须先持久化。

于是:

text 复制代码
修改 Buffer Pool
       ↓
产生 Dirty Page
       +
产生 redo
       ↓
redo 先持久化
       ↓
Dirty Page 后续才能安全刷盘

然后后台:

text 复制代码
不断 Flush Dirty Page
       ↓
Checkpoint 向前推进

如果发生:

text 复制代码
💥 Crash

则:

text 复制代码
找到最近 Checkpoint
       ↓
扫描之后的 redo
       ↓
恢复需要恢复的数据页修改

所以:

text 复制代码
WAL
     ↓
确保恢复信息先存在


Checkpoint
     ↓
确定恢复从哪里开始


redo
     ↓
真正重做 Page 修改

三者并不是三个孤立的知识点。


17. 一张图把正常运行和崩溃恢复连起来

正常运行:

text 复制代码
UPDATE
   ↓
修改 Buffer Pool
   ↓
Dirty Page
   +
redo
   ↓
redo 持久化
   ↓
COMMIT
   ↓
后台 Flush Dirty Page
   ↓
Checkpoint 不断推进

突然:

text 复制代码
 Crash

重新启动:

text 复制代码
读取 Checkpoint LSN
        ↓
扫描后续 redo
        ↓
Redo Log Application
        ↓
恢复 Page
        ↓
识别未完成事务
        ↓
使用 undo 回滚
        ↓
数据库恢复一致

这就是 InnoDB Crash Recovery 的主线。


18. 一个已经 COMMIT 的事务一定能恢复吗?

这里必须加一个很重要的前提:

取决于 redo 是否真正达到了对应的持久化条件。

默认可靠配置下,事务提交时会保证相应日志持久化。

但 MySQL 存在参数:

innodb_flush_log_at_trx_commit

它会影响事务提交时 redo 的写入和刷盘策略。

如果为了性能使用了更弱的持久化配置,那么:

服务器突然掉电时,最近一小段已经返回 COMMIT 成功的事务仍可能丢失。

MySQL 8.4 官方文档也明确指出,弱化日志刷盘策略可能在崩溃时损失最近的事务;对于需要完整 ACID durability 的场景,应使用可靠的日志同步配置。

所以不能把 Crash Recovery 理解成:

无论怎么配置,只要客户端看到 COMMIT,就绝对永远不会丢。

正确理解是:

Crash Recovery 能恢复已经可靠持久化到 redo 中的信息;持久性本身还受到日志刷盘配置影响。


19. 两阶段提交在 Crash Recovery 时又怎么发挥作用?

前面讲 UPDATE 时,我们学过:

text 复制代码
InnoDB PREPARE
       ↓
写 binlog
       ↓
InnoDB COMMIT

现在考虑最危险的情况:

text 复制代码
InnoDB PREPARE
       ↓
binlog 已经成功
       ↓
💥 Crash
       ↓
InnoDB 还没完成最终 COMMIT

重启以后怎么办?

不能简单看到:

PREPARED

就回滚。

因为 binlog 已经成功写入了这个事务。

MySQL 重启恢复时会检查 binary log 中完整记录的事务 XID,并告诉存储引擎:

text 复制代码
prepared transaction

如果 XID 在完整 binlog 中
↓
提交


如果不在
↓
回滚

MySQL 8.4 官方文档明确说明,服务器重启时会扫描最新 binary log 中的事务标识,并让 InnoDB 完成已经成功写入 binlog 的 prepared transaction,从而保证 binlog 和 InnoDB 数据保持一致。

这样上一章的:

两阶段提交

就和这一篇的:

Crash Recovery

真正连起来了。


20. 所以 binlog 会参与 InnoDB 的页面恢复吗?

不会直接替代 redo。

需要区分两个职责。

redo log:

恢复 InnoDB 数据页。

binlog:

在事务协调场景下,帮助确定某些 PREPARED 事务最终应该 COMMIT 还是 ROLLBACK。

所以:

text 复制代码
redo
↓
Page Recovery


binlog
↓
Transaction Commit Coordination

这也是为什么前面我们说:

不能只留下 binlog,然后不要 redo log。

binlog 根本不是 InnoDB 的 Page 级恢复日志。


21. 那 undo log 会不会把已提交事务回滚掉?

不会。

undo log 本身只是:

提供"如何撤销修改"的信息。

最终到底要不要执行 undo,要看事务状态。

例如:

text 复制代码
事务 A:

COMMITTED

那么对应修改应该保留。

不会因为存在 undo 就把它撤销。

而:

text 复制代码
事务 B:

Crash 时仍然 ACTIVE

则需要:

text 复制代码
undo
↓
ROLLBACK

所以:

undo 是撤销能力,不等于所有修改最终都会被撤销。

这和正常执行:

sql 复制代码
ROLLBACK;

是同一个道理。


22. Crash Recovery 完成前 MySQL 一直不能连接吗?

这里实际实现比我们画的简单流程更复杂。

MySQL 官方恢复流程中,redo log application 会在初始化阶段执行,并且是在接受连接之前完成。

但 redo application 之后,InnoDB 会尽可能早地开始接受连接。

例如:

未完成事务的 rollback

可以由后台线程继续执行。

也就是说,实际情况可能是:

text 复制代码
redo recovery 完成
       ↓
开始允许新的连接
       ↓
后台继续 rollback 某些旧事务
       ↓
后台继续 purge 等工作

因此如果崩溃前存在一个非常大的未提交事务:

MySQL 虽然可能已经可以提供服务,但后台 rollback 仍然可能持续很长时间。

官方文档也指出,Crash Recovery 中对未完成事务的 rollback 可以在接受新连接后由后台线程继续执行,并且恢复中的事务在 rollback 完成前仍可能导致锁冲突。


23. 一个超大事务为什么会让恢复很慢?

假设执行:

sql 复制代码
START TRANSACTION;

UPDATE huge_table
SET status = 1;

一次修改了几千万行。

执行很久以后:

text 复制代码
还没有 COMMIT

突然服务器崩溃。

重启以后:

text 复制代码
redo
↓
恢复必要页面

接下来发现:

text 复制代码
这个超大事务没有提交

那么就必须:

text 复制代码
undo
↓
逐步回滚大量修改

显然会很慢。

MySQL 官方甚至说明,大型未完成事务的 rollback 时间可能明显长于事务之前运行的时间。

这也是为什么我们一直强调:

尽量避免超长、超大的事务。

它不仅会:

  • 长时间持有锁
  • 保留大量 undo 历史
  • 影响 MVCC
  • 增加日志压力

还可能:

让崩溃后的恢复和 rollback 变得非常慢。


24. redo 能解决所有磁盘写入损坏问题吗?

不能。

这里再补一个非常重要的概念:

Doublewrite Buffer

假设一个 Page 默认是:

16KB

现在 InnoDB 正准备把整个 Page 写入数据文件:

text 复制代码
16KB Page

结果只写了一半:

text 复制代码
写了 8KB
↓
突然断电

那么这个 Page 可能变成:

Torn Page

也就是:

页面本身只写了一部分,结构已经损坏。

这和普通的:

text 复制代码
整个 Page 还是旧版本

不是一回事。

InnoDB 为这种问题提供了 Doublewrite Buffer。


25. Doublewrite Buffer 是怎么工作的?

在 Page 真正写到数据文件对应位置之前,InnoDB 会先把 Page 写到:

Doublewrite Buffer

大致:

text 复制代码
Dirty Page
    ↓
先写 Doublewrite Buffer
    ↓
确认完成
    ↓
再写真正 data file

如果第二次写真正数据文件时:

text 复制代码
写了一半
↓
💥 Crash

恢复时可以从 Doublewrite Buffer 找到一个完整 Page 副本进行修复。

MySQL 8.4 官方文档明确说明,doublewrite buffer 就是用来在 Page 写入中途发生系统、存储或 mysqld 崩溃时,恢复这种 incomplete page write 的。

所以:

redo log 和 Doublewrite Buffer 也不是一回事。


26. redo 和 Doublewrite 分别解决什么?

可以这样理解。

普通情况:

text 复制代码
Page 完整
但是版本比较旧

↓
redo

把缺失修改重做

而 Torn Page:

text 复制代码
一个 16KB Page
只写进去一部分

↓
Page 本身已经损坏

↓
Doublewrite Buffer
提供完整 Page 副本

因此:

text 复制代码
Doublewrite
↓
先解决 Page 是否完整


redo
↓
再解决 Page 是否包含最新需要恢复的修改

这两个机制配合,Crash Recovery 才更加可靠。


27. Crash Recovery 的完整流程怎么理解?

可以概括成:

text 复制代码
                MySQL Crash
                     ↓
                重新启动
                     ↓
             找到 Tablespace
                     ↓
            找到 Checkpoint LSN
                     ↓
              扫描 redo log
                     ↓
            Redo Log Application
                     ↓
           恢复必要的数据页修改
                     ↓
          处理 PREPARED 事务状态
                     ↓
      binlog / transaction coordinator
        帮助判断最终提交或回滚
                     ↓
           回滚未完成事务
                     ↓
                 undo
                     ↓
           后台继续 purge 等工作
                     ↓
              数据库恢复正常

真实 MySQL 内部还有更多步骤,例如 tablespace discovery、change buffer merge、purge 等,但我们现在最应该掌握的是:

text 复制代码
Checkpoint
    ↓
Redo
    ↓
Transaction State
    ↓
Undo

这条主线。MySQL 官方列出的 Crash Recovery 过程也包括 tablespace discovery、redo log application、rollback incomplete transactions、change buffer merge 和 purge 等步骤。


28. 正常关闭为什么通常不需要这么重的恢复?

如果 MySQL 正常关闭:

text 复制代码
正常 Shutdown

InnoDB 有机会完成必要的日志和后台处理。

因此下一次启动通常不需要像异常 Crash 那样处理大量 redo recovery。

InnoDB redo 实现文档也指出,在正常 shutdown 情况下,redo 在逻辑上通常已经不再存在需要从 checkpoint 之后重新应用的记录;而某些 fast shutdown 模式会把更多恢复工作留到下次启动。

所以:

text 复制代码
正常关闭

和:

text 复制代码
直接 kill / 断电

不是同一种启动恢复成本。


29. Crash Recovery 和备份恢复是一回事吗?

不是。

这个也非常重要。

Crash Recovery:

数据库自己发生异常宕机以后,利用 redo、undo 等机制恢复当前数据库。

例如:

text 复制代码
服务器突然断电
↓
重启 MySQL
↓
InnoDB 自动 Crash Recovery

而:

Backup Recovery

或者:

Point-In-Time Recovery

解决的是:

数据文件真的丢了、磁盘损坏了,或者用户误删数据以后怎么办?

例如:

sql 复制代码
DROP TABLE important_table;

这是一个正常执行并提交成功的操作。

Crash Recovery 不会说:

"这是误操作,我帮你撤销。"

因为从数据库角度:

text 复制代码
事务正常执行
正常提交

它没有崩溃一致性问题。

这种情况需要:

text 复制代码
Backup
+
binlog
↓
PITR

恢复。

所以:

text 复制代码
Crash Recovery
≠
Backup Recovery

这是两套不同的问题。


30. redo log、undo log、binlog 怎么区分?

redo log 属于 InnoDB。

核心作用:

Crash Recovery

主要解决:

应该存在的 Page 修改还没完整写入数据文件怎么办?


undo log 属于 InnoDB。

核心作用:

ROLLBACK + MVCC

在 Crash Recovery 中主要解决:

未提交事务的修改已经进入数据页怎么办?


binlog 属于 MySQL Server。

核心作用:

Replication + PITR

同时在 InnoDB 两阶段提交恢复中,还可以帮助:

判断某些 PREPARED 事务最终应该提交还是回滚。

所以:

text 复制代码
redo
↓
重做


undo
↓
撤销


binlog
↓
记录事务的数据变化

三个日志各有职责。


相关推荐
盟道科技1 小时前
电商小程序购物车系统设计:缓存与数据库双写一致性实战
数据库·缓存·小程序
java1234_小锋1 小时前
【技术专题】Mysql8 数据库 - Mysql8 客户端sqlyog安装以及配置
数据库·sqlyog
奇牙coding1 小时前
Codex C接 配置教程:的 字段从迁移时必须写完整后缀,填旧值或省略 会静默回退默认模型
java·c语言·数据库·ai
Nturmoils1 小时前
别急着 JOIN,子查询有些场景更顺手
数据库
喜欢的名字被抢了1 小时前
09-Redis 进阶原理篇:单线程、多线程、过期、LRU-LFU、Fork 与 Lua
数据库·redis·lua
Omics Pro1 小时前
计算虚拟扰动:网络毒理+虚拟敲除
数据库·人工智能·算法·机器学习·自然语言处理
白帽攻防录10 小时前
SRC 挖洞:Roundcube 预认证 SQL 注入深度复盘,CVE-2026-48842 preg_replace 转义绕过怎么打穿邮件系统
网络·数据库·sql·网络安全·sql注入
꯭自꯭闭꯭10 小时前
达梦SQL优化相关
linux·运维·数据库·sql
nhdh10 小时前
B+树揭秘:MySQL索引核心原理全解析
数据结构·b树·mysql