一次机房断电后,一个"已提交"的订单在数据库里消失了。开发说"事务提交了就一定持久",DBA 指着配置说"innodb_flush_log_at_trx_commit=0"——于是"提交"只是"写进了内存缓冲",断电就没了。
"提交即持久"这句话有个前提:日志真的落盘了。而"落盘"由一组参数决定,它们默认"照顾性能"——于是"你以为的持久"和"实际的持久"差了一次 fsync.
1. 提交路径上的三次"缓冲"
| 层 | 缓冲位置 |
|---|---|
| 应用/驱动 | 语句缓冲(批量时才刷) |
| 数据库内存 | redo log buffer |
| 操作系统 | page cache(fsync 前) |
第三项是"最容易被忽略"的:"写文件"只是"写进 page cache"第三项的"日常表现"是"看起来一切正常":"写完立刻读"能读到(因为读的也是 page cache)——**于是"没有 fsync 的写入"在"正常运行"时"完全正确".
——断电时它还没到磁盘.
"三个开关"的正确组合只有一种,而它常被"性能调优"改掉:
// 一个"要持久"的配置(三行)
innodb_flush_log_at_trx_commit = 1 # 提交即 fsync
sync_binlog = 1 # binlog 每次同步
// 磁盘:关写缓存(云盘选"高可靠",或本地 SSD)
// 判据:这三行"任何一行被放宽",就不再是"提交即持久"
2. "提交即持久"的三个开关
| 开关 | 设置 |
|---|---|
innodb_flush_log_at_trx_commit |
1=提交即 fsync(默认,持久) |
sync_binlog |
1=每次提交同步 binlog |
| 存储/云盘的写缓存 | 关掉(否则 fsync 也骗人) |
第一项的"三档"要记清:1 = 每次提交 fsync;2 = 每次提交"写到 OS"(每秒 fsync);0 = "每秒写到 OS + fsync"——**只有 1 是"提交即落盘".
第一项是"最关键"的:设 0 时"每秒刷一次"——**于是"最多丢 1 秒",而"提交"只保证"进了内存".
第三项是"硬件层"的坑:**云盘"写缓存开着"时,fsync 返回了但"数据还在盘的内存里"**第三项的"排查"要"看盘"而不是"看库":云厂商的"普通云盘"常带写缓存——**要"持久"就得换"本地 SSD 或 高可靠盘",且确认"关缓存".
——断电照样丢.
3. "丢了哪一种"的三种情形
| 情形 | 现象 |
|---|---|
| 丢"最近 1 秒" | 参数放宽(0/2) |
| 丢"整个事务" | 日志未刷 + 缓冲池脏页 |
| 主从"不一致" | binlog 未同步 |
第二项的"修复可能"取决于"备份链":若 binlog 每条都有,可以"按时间点恢复"——若"binlog 也在内存里",那就是"真丢了".
第二项最"整":"一个事务的日志没刷,而它也没进 binlog"**——于是"恢复时"整个事务不存在.
"组提交"是"兼得"的唯一手段,而它常被误认为"放宽":
// 三种"提交方式"的代价
逐个 fsync:每次 1–5ms → 高频写扛不住
放宽到 0/2:延迟好,丢 1 秒
组提交(group commit):多个事务"共用一次 fsync" → 仍然"提交即持久"
// 判据:要"又快又不丢",答案是"组提交",不是"关掉 fsync"
4. "恢复"靠的三个东西
| 依赖 | 作用 |
|---|---|
| redo log | 重放"已提交但未落数据文件"的修改 |
| undo log | 回滚"未提交"的修改 |
| binlog | 主从复制与"按时间点恢复" |
第一项是"崩溃恢复"的主力:**"数据文件"可以落后,只要"redo 完整"**第一项的"设计意图"就是"日志先行":先写日志、再改数据页(WAL)——**于是"崩溃后"用日志"重放"即可,而不必"每次提交都写数据页".
——**于是"日志比数据文件更重要".
"云盘"这条要"看指标",因为"厂商说的高可靠"不等于"关缓存":
// 三步"确认盘是否真落盘"
1 看云盘类型:普通云盘 vs 本地 SSD vs 高可靠盘
2 查"是否允许关写缓存"(有的盘"不允许",fsync 只是"进了盘缓存")
3 做"断电演练":拔电/重启,检查"最后 N 笔"
// 判据:演练通过才算"持久",配置通过不算
5. "为什么要放宽参数"的三种理由
| 理由 | 代价 |
|---|---|
| 写入吞吐(高频小事务) | 丢 1 秒 |
| 延迟(fsync 每次 ~1ms+) | 延迟抖动 |
| 批量导入 | 可接受 |
第二项在"云盘"上更明显:fsync 一次可能"几毫秒到几十毫秒"——**于是"每提交一次"的延迟被"放大".
"主从不一致"的根因常在"binlog 刷盘",而它有两个参数要说清:
// 两种"binlog 刷盘"
sync_binlog=1:每次提交"fsync binlog"(最稳,最慢)
sync_binlog=0:交给 OS(快,但"崩溃"可能丢"未刷的部分")
// 判据:主库"提交成功"而"从库没有" → 先查这一项
6. 排错:五种"持久性问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 断电丢"已提交" | flush_log=0 |
改 1 |
| 主从不一致 | sync_binlog≠1 |
改 1/权衡 |
| 云上偶发丢数据 | 盘写缓存 | 关缓存 |
| 恢复后"少了半天" | binlog 未备份 | 补备份链 |
| 提交慢 | fsync 频繁 | 批量/组提交 |
"批量导入"是"临时放宽"的正当场景,而它要"改回去":
// 三步"导入期放宽、导入后收紧"
1 导入前:flush_log_at_trx_commit=0(吞吐优先)
2 导入后:改回 1,并"手动 flush"一次
3 验收:重启演练 + "最后 N 笔"检查
// 判据:放宽必须"有明确的恢复动作",否则它会"永久生效"
7. 边界:什么情况"放宽"可以接受
| 场景 | 为什么 | 做法 |
|---|---|---|
| 日志/埋点(可丢) | 丢 1 秒无感 | 可放宽 |
| 缓存/会话(可重建) | 丢了再算 | 可放宽 |
| 批量导入(可重跑) | 失败重来 | 临时放宽 |
| 账务/订单(不可丢) | 丢一条就是事故 | 必须 1 |
最后一条与"验收"有关的边界:"持久性"要"演练"而不是"读配置":"kill -9 数据库进程"后重启,检查"最后 100 笔提交"是否都在——配置写着 1,而"盘缓存开着"时,演练会立刻暴露它.
判断口径一句话:"丢 1 秒的数据,业务能不能接受"**。不能(订单、支付、库存变更),就必须"flush_log=1 + sync_binlog=1 + 盘缓存关闭";能(日志、统计),放宽是"用可能性换吞吐"的正当选择——而"没想清楚就跟着默认走"最危险.
再补一个"应用层"的坑:"批量插入 1000 条提交一次"看起来"省 fsync"——但"事务越大","崩溃后"要"重放/回滚"的量也越大,于是"恢复时间"被拉长。所以"大批量"要"分批 + 每批提交",而"每批"的 fsync 成本又被"组提交"摊薄——**"批大小"与"持久性"是两条独立的线,不该用"放宽 fsync"去解决"吞吐".
还有一个"顺序"事实值得记:binlog 与 redo 的写入顺序由"两阶段提交"协调——先写 redo(prepare)、再写 binlog、最后写 redo(commit)——于是"崩溃"时能判断"这个事务该提交还是回滚"。这也是为什么"sync_binlog≠1"会造成"主从不一致":主库"redo 提交了",而 binlog "没刷",从库就没收到.
而"两阶段提交"也解释了"为什么不能只留一份日志":binlog 服务"复制与按时间点恢复",redo 服务"崩溃恢复"——两者职责不同,缺一个都会"在某类故障下丢数据".
所以"两份日志"不是冗余,是分工。
而"分工"这件事,比"参数调优"更值得先想清楚。
8. 自测
- 提交路径上的三次缓冲分别在哪?为什么"写文件"不等于"落盘"?
- 三个开关里哪一项最关键?设 0 时"最多丢多少"?
- 为什么"云盘写缓存"会让 fsync 也"骗人"?
- "恢复"靠的三个东西分别做什么?为什么"日志比数据文件更重要"?
- "放宽参数"的三个理由分别对应什么代价?
- 判断"能否放宽"的那个问题是什么?