2177 字
约 7 分钟
16
S11-A25 · 崩溃恢复:为什么"提交了"的数据会消失
2024-12-31
2026-11-10

一次机房断电后,一个"已提交"的订单在数据库里消失了。开发说"事务提交了就一定持久",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. 自测

  1. 提交路径上的三次缓冲分别在哪?为什么"写文件"不等于"落盘"?
  2. 三个开关里哪一项最关键?设 0 时"最多丢多少"?
  3. 为什么"云盘写缓存"会让 fsync 也"骗人"?
  4. "恢复"靠的三个东西分别做什么?为什么"日志比数据文件更重要"?
  5. "放宽参数"的三个理由分别对应什么代价?
  6. 判断"能否放宽"的那个问题是什么?
S11-A25 · 崩溃恢复:为什么"提交了"的数据会消失
https://www.clxhxhhr.top/posts/6688/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。