一、事务基本过程
1、修改内存页(在 Buffer Pool 里完成)。
2、把对应的 redo log 记录写到 redo log buffer(内存)。
3、事务提交时,redo log buffer 先刷盘(顺序写 redo log 文件)。
3.1、如果开启 binlog,redo、binlog 2PC 提交,保证原子性。
4、然后再把脏页(16 KB)写到 doublewrite buffer(磁盘 2 MB 区)。
5、最后才把同一份 16 KB 页写到真正的数据文件。
所以 redo log 先于 doublewrite buffer 落盘,但 redo log 本身不携带整页数据,只记录“增量”。redo log 管逻辑,doublewrite buffer 管物理完整。
二、整体过程
1、语句执行阶段
-> 先在 Buffer Pool 修改内存、生成 undo、生成 redo 记录(仍然只在内存)。
2、收到客户端 COMMIT
-> 立即把该事务的 redo log buffer 刷盘(fsync)
-> 成功后向客户端返回「Query OK」
3、之后
-> 脏页异步刷盘(内存脏页 -> doublewrite,同一份内存脏页 -> 数据文件)
三、undo log、redo log、doublewrite buffer
redo log 包含「数据页」和「undo页」的变更。
1、生成阶段
修改数据页 → 生成 数据页 的 redo 记录
同时插入 undo 记录 → 生成 undo 页 的 redo 记录
这两类 redo 记录混在同一条 redo log buffer 里。每条 redo 自带长度字段,fsync 只会刷“连续且完整”的 redo 记录。也就是这两部分写入具有原子性。
2、刷盘阶段
redo log 刷盘(fsync)只按 LSN 顺序一次性把 buffer 里的所有记录(数据+undo)写盘。
「数据页」和「undo 页」本身仍作为普通脏页,稍后由后台线程异步刷盘。
3、redo log
格式示例:(type=MLOG_REC_UPDATE, space_id=6, page_no=12345, offset=0x3A0, old_bytes=0x11, new_bytes=0x22)
作用:记录物理页上某个偏移量的字节如何变化(增量)
redo log 重放日志,ib_logfile0、1(固定 2 个文件,循环写)
需要依赖「完整页」才能重放,需要 doublewrite buffer 提供完整的页镜像。
4、undo log
一般称为回滚日志、撤销日志,MySQL 5.7 默认把 undo log 放在系统表空间 ibdata1 里。
格式示例:(trx_id=1001, roll_ptr=0x00…7F, op=UPDATE, table_id=18, old_row=<完整旧行>)
作用:记录逻辑旧行整行内容,用于回滚或 MVCC 读取旧版本。
undo 页刷盘只是“把内存里的垃圾搬到磁盘”,回滚的正确性早就由 redo log 保证。一条事务 commit 后,undo log 仍然保留一段时间,因为:
(1)MVCC 需要
其他事务可能正在用你的旧版本数据做一致性读;
只有所有活跃事务都不再引用这些旧版本时,undo 记录才能被 purge。
(2)purge 线程异步清理
当旧版本无人引用,purge 线程把对应的 undo 记录标记为可重用,并最终把 undo 页刷盘(写覆盖,空间回收)
5、doublewrite buffer
双写缓冲,位于系统表空间 ibdata1 内部,固定偏移量(2 MB 连续区域),默认路径与 datadir 一致。
保存脏页的完整 16 KB 物理数据(整页内容),用于崩溃修复“半页损坏”。
四、redo log 落盘怎么保证原子性?
1、记录完整性
每条 redo 记录自带 长度字段,写完才更新尾部指针;fsync 只刷到「完整记录」边界,不会半条落盘。
保证写的那一刻是完整的。
2、顺序追加写
redo log 是固定环形文件,顺序追加无寻道;磁盘控制器通常能保证 512B / 4KB 原子写。
消费级 SSD 都有掉电保护电容,保证 4KB 原子写;在支持 16KB 原子写的企业级别 SSD 上,可以关闭 Doublewrite Buffer,仍能保证页级完整性(InnoDB 16KB 页)。
3、崩溃恢复校验
重启时按 LSN 扫描,读到不完整记录直接丢弃,保证不会应用损坏的 redo。损坏部分可以视为未提交。
五、事务等待 redo log 刷盘会慢吗?
也就是 innodb_flush_log_at_trx_commit = 1,不会「慢到不可接受」,原因有三:
1、顺序写 + 追加写
redo log 是固定大小的环形文件,每次只把几十字节 ~ 几 KB 的记录顺序追加到最后,无磁盘寻道,现代 SSD 顺序写延迟 < 100 μs。
2、组提交(Group Commit)
多个并发事务的 redo log 在一次 fsync 中合并刷盘,平均到每事务的代价进一步摊平。MySQL 5.7默认开启。
流程图(简化):
# 同一毫秒内到来的多个事务共享一次 fsync
┌──事务A commit──┐
└─加入队列 LSN=100
┌──事务B commit──┐
└─加入队列 LSN=102
▼
log_flusher 线程
fsync(LSN≤102) 一次
▲
┌──事务A返回
└──事务B返回
3、参数可控
innodb_flush_log_at_trx_commit = 1 (默认)保证每次 commit 立即 fsync,延迟约 0.1 ~ 0.5 ms;支付链路永远 1,不能因为量少就冒险。这个参数决定 redo log 的刷盘时机。
若能接受 1s 丢数据,可设为 2,延迟降到微秒级(非核心业务/日志库)。
因此,即使高并发,单条事务 commit 的刷盘延迟通常 < 1 ms,不会成为瓶颈。
六、binlog 是如何落盘
binlog 刷盘策略 sync_binlog = N:
N 表示 每 N 个事务或每 N 次 write 后执行一次 fsync.
1 表示每次事务提交立即刷盘(5.7 默认。最安全,也最慢)
0 表示由操作系统决定何时刷盘(最快,由丢失风险)
redo log 的落盘改为二阶段提交(2PC),sync_binlog = 1,落盘顺序是:
1、prepare
先写 redo log(innodb_flush_log_at_trx_commit=1),事务状态标为 prepare。
2、write / flush binlog
再把 binlog 写到 binlog 文件(拷进内核 PageCache)并 fsync(落盘)
3、commit
redo log 里把事务状态改为 commit。
崩溃恢复时,InnoDB 按 binlog 是否存在且完整来判断:
redo 已 prepare、binlog 存在且完整 -> 提交
redo 已 prepare、binlog 不存在或不完整 -> 回滚。
注意:开启 binlog,一般要做数据恢复或者从库同步。如果不用「双1」,不能保证 redo、binlog 顺序,可能出现「binlog 有,redo 无」。
此时会出现 binlog 超前:
场景:主库发现没有 redo -> 事务视为未提交,数据还是旧值;从库收到 binlog -> 数据变成新值。
结果:从库比主库多一条记录。如果是日志库,永久容忍这条差异没问题;如果是关键业务库可能要逆向 SQL 恢复、重做从库或者使用 pt-table-sync 把差异补齐。