Skip to content

08|InnoDB 存储引擎

前面建表时都写了 ENGINE=InnoDB。为什么用 InnoDB?它和别的存储引擎有什么区别?为什么 MySQL 断电重启后数据不丢、为什么能回滚事务、为什么有行锁——这些都和 InnoDB 的内部机制有关。

本篇讲 InnoDB 存储引擎的几个核心:Buffer Pool(内存缓存)、Redo Log(崩溃恢复)、Undo Log(回滚)、表空间。理解了这些,出问题时能判断该往哪个方向查。

一、Buffer Pool:MySQL 的工作台

InnoDB 读写数据不直接操作磁盘——太慢了。它先把数据从磁盘读到内存里的一块区域,在内存里改,改完再刷回磁盘。这块内存就是 Buffer Pool

可以理解成工作台。磁盘像书架,书(数据)平时在书架上。要看一本书,先从书架拿到工作台上。工作台越大,能同时摊开的书越多。工作台太小,看几页就要放回书架、再拿新的,大部分时间花在"来回拿"而不是"看"上。

工作台够不够大

sql
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';

两个指标:

  • Innodb_buffer_pool_read_requests — 总共要从 Buffer Pool 读多少次(逻辑读)
  • Innodb_buffer_pool_reads — 其中多少次 Buffer Pool 没有,得去磁盘拿(物理读)

命中率:

text
(1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) × 100%

99% 以上才算健康。低于 99%,说明工作台太小,大部分时间花在从磁盘搬数据上。

调多大

sql
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

机器只跑 MySQL 给到物理内存 70%-80%。机器还跑别的服务要留一些。8.0+ 支持在线调,不用重启:

sql
SET GLOBAL innodb_buffer_pool_size = 17179869184;  -- 16G

出问题时的表现

查询本身不慢(EXPLAIN 走索引了),但就是响应慢——大概率 Buffer Pool 太小,频繁去磁盘读。但注意:EXPLAIN 显示 type = ALL(全表扫),先解决索引问题,不是 Buffer Pool。先看索引,再看 Buffer Pool,顺序别反。

二、Redo Log:干完活的底单

Buffer Pool 在内存里改数据快,但有个问题:机器突然断电,内存里的数据全丢了怎么办?

InnoDB 靠 redo log 解决。改数据时,InnoDB 先把"改了哪几页数据"记到 redo log(只追加写的文件),再改内存里的 Buffer Pool。

理解成快递底单:寄包裹时快递员先让你填一张底单(日志),底单落袋了,包裹后面慢慢送。万一包裹丢了,凭底单可以重新发一份。

redo log 的作用是 MySQL 崩溃后,通过重放 redo log 把已提交但没写回磁盘的数据恢复回来。 这个过程叫 crash recovery,重启时自动做,不用人工干预。

一个关键参数

sql
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

默认 1,每次事务提交都把 redo log 刷到磁盘。最安全——但每次提交都要写磁盘,写入量大时对 IO 有压力。改成 2 表示每秒刷一次,OS 崩溃时可能丢 1 秒数据。核心业务库保持 1,日志类、可接受少量丢数据的库才考虑调低。

redo log 和 binlog 的区别

这两个经常搞混,职责完全不同:

redo logbinlog
谁管的InnoDB 引擎层MySQL Server 层
记什么哪个数据页被改了哪条 SQL 或哪行变更被提交
干什么用崩溃恢复主从复制、时间点恢复
要不要人工管不用,自动要,备份和清理策略

崩溃恢复靠 redo,复制和恢复靠 binlog。 redo 是快递员的底单(保证包裹不丢),binlog 是你的寄件记录(可以查某天寄了什么)。

三、Undo Log:后悔药

开了个事务,先插了一条数据,又觉得不对想回滚。InnoDB 怎么知道插入之前是什么状态?靠 undo log

undo log 记录的是修改前的老数据。插入前记下"这里原来是空的",更新前记下"原来是什么值"。回滚时按 undo log 把数据改回去。

undo log 还有一个重要用途——MVCC(多版本并发控制)。一个事务在改数据,另一个事务同时读这条数据,读到的应该是改之前的值(没提交的不能读)。undo log 存了老版本,让读操作能拿到一致性的快照,不用等写操作完成。这就是为什么 InnoDB 读写不互相阻塞(行锁不阻塞普通读)。

undo log 和长事务

undo log 要保留到事务结束(回滚或提交后才能清理)。如果一个事务开着一直不提交(长事务),它对应的 undo log 一直占着空间,表空间越来越大,性能下降。

sql
-- 看有没有跑很久的事务
SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started ASC LIMIT 10;

看到跑了几个小时的事务要排查——多半是应用开了连接没提交(忘了 commit)或连接池泄漏。

四、表空间

InnoDB 的数据存在表空间里。每张表的数据存哪:

sql
SHOW VARIABLES LIKE 'innodb_file_per_table';

ON(8.0 默认):每张表单独一个 .ibd 文件,存在 datadir/库名/表名.ibd。管理和备份方便,删表直接删文件释放空间。

OFF:所有表共享一个系统表空间(ibdata1)。删表不释放空间,文件越来越大。不推荐。

8.0 默认每表一个文件,不用管这个参数。但要记着数据存在哪——排查磁盘满、要单独备份某张表时知道去哪找。

五、这几个机制怎么配合

一条 UPDATE 从执行到完成,InnoDB 内部走了一遍:

text
1. 从磁盘读数据页到 Buffer Pool(如果不在内存)
2. 记 undo log(记下改之前的值,用于回滚和 MVCC)
3. 改 Buffer Pool 里的数据页
4. 记 redo log(记下改了哪个页,用于崩溃恢复)
5. 记 binlog(Server 层记变更,用于复制和恢复)
6. 提交事务,redo log 刷盘(双 1 配置下)

后面 Buffer Pool 里的脏页(改了但没刷盘的页)会在后台慢慢刷回磁盘表空间。断电了不怕——redo log 在磁盘上,重启时重放恢复。

理解这个流程,就能解释很多现象:为什么断电不丢已提交的数据(redo log)、为什么能回滚(undo log)、为什么长事务有问题(undo log 不清理)、为什么改数据要先读进内存(Buffer Pool)。

存储引擎之后

知道了 InnoDB 怎么存数据、怎么恢复、怎么回滚。但多个事务同时操作时怎么不互相干扰——事务和锁——下一篇讲。这是"为什么有锁等待、为什么会死锁"的根因。