先走一遍完整的更新流程
日志分散在不同的层里,拿这条语句做例子,id 是主键,执行前这一行的 name 是 'a':
UPDATE t SET name = 'b' WHERE id = 1;
从客户端发出去到最终落盘,中间是这些步骤:
Server 层
连接器 校验账号密码和权限
分析器 词法语法分析,知道这是一条 UPDATE,表是 t,条件是 id = 1
优化器 决定走主键索引
执行器 调用 InnoDB 的读写接口
InnoDB 层
1. 从 buffer pool 里找到 id = 1 这一行,页不在内存就从磁盘读进来
2. 写 undo log,记下"这一行的 name 原来是 'a'"
3. 修改 buffer pool 里这一行,页变成脏页
4. 写 redo log,记下"在某个页的某个位置把 name 改成了 'b'"
注意这个动作在执行过程中就在做,先进 redo log buffer
提交
5. redo log 按 innodb_flush_log_at_trx_commit 的策略刷盘
6. binlog 从 binlog cache 写进文件,按 sync_binlog 的策略刷盘
两个时间点要分清楚:redo log 是事务执行过程中一直在写的,binlog 是提交的那一刻才落盘的。第 4 步只写进内存,第 5 步才是真正决定"这个事务丢不丢"的地方。
几种日志的对照
| 日志 | 归属 | 记的是什么 | 干什么用 |
|---|---|---|---|
redo log | InnoDB 引擎层 | 物理逻辑日志,对数据页做了什么修改 | 崩溃恢复 |
undo log | InnoDB 引擎层 | 逻辑日志,修改前的旧值或反向操作 | 事务回滚、MVCC 快照读 |
binlog | Server 层 | 逻辑日志,语句或行的变更 | 主从复制、按时间点恢复 |
relay log | Server 层(从库) | 从主库拉过来的 binlog | 从库回放 |
error log | Server 层 | 错误、警告、启停信息 | 排障 |
slow query log | Server 层 | 超过 long_query_time 的语句 | 性能分析 |
general log | Server 层 | 所有语句 | 调试,默认关闭 |
前面三个是重点,后面几个是运维用的,和事务、崩溃恢复没关系。
三组容易混的关系
redo log 和 binlog
这两个是最常被放在一起问的,从四个维度对比:
| 维度 | redo log | binlog |
|---|---|---|
| 归属 | InnoDB 引擎层,MyISAM 就没有 | Server 层,所有引擎都有 |
| 内容 | 物理逻辑日志,对数据页的修改 | 逻辑日志,语句或行的变更 |
| 写入方式 | 循环写,空间固定,写满就覆盖 | 追加写,写完一个文件换下一个,不覆盖 |
| 何时写 | 事务执行过程中持续写 | 事务提交时才刷到文件 |
| 用途 | 崩溃恢复 | 主从复制、按时间点恢复 |
一句话概括:redo log 保证"已经提交的事务不丢",binlog 保证"数据能被复制和重放"。
redo log 和 undo log
一个往前,一个往后:
redo log是前滚,崩溃恢复的时候把已提交但没落盘的修改重做一遍undo log是回滚,事务失败或者崩溃恢复的时候把不该留的修改撤掉
redo 记的是"要做什么",undo 记的是"怎么撤回去"。两者的细节分别在 Redo Log:MySQL 为什么能够实现崩溃恢复?和 Undo Log:事务回滚是怎么实现的?两篇里展开。
为什么一定要 WAL
反过来想:如果不写 redo log,事务提交的时候直接把数据页刷回磁盘,会怎样?
数据页是 16KB,改一行也得把整页写回去;页在磁盘上的分布是随机的,写一次就是一次随机 IO;一个事务改了好几个页,就要好几次随机 IO 才能提交。磁盘的随机 IO 性能比顺序写差一两个数量级,这个代价扛不住。
WAL(Write-Ahead Logging,预写日志)换了个方向:先顺序写日志,数据页交给后台慢慢刷。写日志是追加,快;数据页攒一批再刷,多次随机写能合并成一次。
代价就是数据页会落后于日志。这个"落后"正是 redo log 存在的理由,也是下面一篇要讲的东西。
不同的语句产生的日志不一样
回到标题这个问题,按语句类型分:
| 语句 | undo log | redo log | binlog |
|---|---|---|---|
SELECT | 不写 | 不写 | 不写 |
INSERT | 写,记的是"删掉这一行" | 写 | 写 |
UPDATE | 写,记旧值 | 写 | 写 |
DELETE | 写,记整行内容 | 写 | 写 |
三种写操作里,INSERT 的 undo log 稍微特别一点:它不需要记旧值(原来根本没有这一行),记的是主键,回滚的时候按主键把这行删掉。所以 undo log 记的不一定是"旧值",准确说是"反向操作"。
SELECT不产生这三种日志,但它不是完全不碰日志。一个跑了很久的SELECT会一直读某个版本的 undo log,只要它还没结束,这个版本就不能被清理,undo 表空间会一直涨。这属于 MVCC 的话题,可以回顾一下 MVCC 的版本链。
DDL 语句(ALTER TABLE 这类)不写 undo log,因为 DDL 不能回滚,它的日志行为和上面这套是分开的,不在这里展开。