一次灵异扣款
聊完优化器,篇篇都是查询。老王上周遇到的事更吓人:机房闪断重启后,用户反馈「余额扣了,订单没了」。查了一圈不是 Bug——两个更新在两个事务里,扣款事务提交了,订单事务还没来得及提交就赶上断电。这个问题暴露的是团队对事务的理解停留在「begin 和 commit 之间自动全对」。ACID 不是口号,是两本日志撑出来的:redo log 和 undo log。
先厘清:ACID 各归谁管
| 特性 | 靠什么实现 |
|---|---|
| 原子性 Atomicity | undo log:做一半崩溃时,把改过的全部撤销 |
| 持久性 Durability | redo log:提交了就不许丢,崩溃后能重放恢复 |
| 隔离性 Isolation | 锁 + MVCC(第 8、9 篇) |
| 一致性 Consistency | 由前三者共同保证的目的,不是独立手段 |
redo log:先把账记下来
一个事务改了十条数据,分散在十个不同的数据页里,每个页都在 B+ 树的不同位置——直接把十个页刷盘就是十次随机写,太慢。InnoDB 的解法是 WAL(Write-Ahead Logging,先写日志):事务提交时只把「哪个页做了什么物理修改」顺序追加进 redo log(顺序写,极快),数据页先留在内存的 Buffer Pool 里慢慢刷。
崩溃了怎么办?重启后把 redo log 重放一遍,没来得及落盘的修改全部补上——顺序写的日志,换随机写的数据页,持久性就是这么买到的。redo log 是固定大小的文件组,循环写:write pos 一直往前追,checkpoint 之后的区域是待写入空间,追上 checkpoint 就得停下来先刷脏页推进 checkpoint——这也是为什么 redo 设太小的实例高峰期写入会周期性抖动。
控制「提交时日志刷多严」的参数,是事务安全和性能的三角:
innodb_flush_log_at_trx_commit = 1 # 每次提交都 fsync 落盘,最安全(默认)
# = 2 提交写到操作系统缓存,每秒 fsync:进程崩溃不丢,断电最多丢 1 秒
# = 0 每秒才写才刷:MySQL 进程崩了也可能丢 1 秒和 binlog 的 sync_binlog 组合出来的「双 1 配置」,第 10 篇细说。
undo log:后悔药
undo log 记录的是逻辑反操作:insert 记下主键好回滚时删除,update 记下旧值好回滚时还原。它有两个职责:一是事务回滚时把数据还原;二是给 MVCC 提供旧版本——其他事务读这行时,如果看到的是历史版本,版本就顺着 undo log 的链找(伏笔,下一篇的主角)。
提交后 undo 不会立刻删,要等没有事务再需要这些历史版本时,由 purge 线程清理。由此得出一条重要的工程纪律:别开长事务——一个几小时不提交的事务,会让它之后的所有 undo 都清不掉,历史版本堆积、表空间膨胀、查询变慢,连坐全库。
崩溃恢复的完整剧本
现在能回答「断电后 MySQL 重启发生了什么」:
- 前滚(redo):重放 redo log,把已提交、但数据页还没刷到磁盘的修改补齐。
- 回滚(undo):找出崩溃时还提交中的事务,用 undo log 把它们改过的数据全部撤销。
一句话:redo 负责让已提交的必达,undo 负责让未提交的作废。两本日志一配合,原子性和持久性同时成立。回头看老王的灵异扣款:它不是事务失效,是两个业务动作被错误地拆成了两个事务——该在一个事务里的操作,永远放进一个事务。
redo 和 undo 的地基打好,下一楼就是隔离级别与 MVCC:不同事务同时读写一行时,各自看到的到底是什么版本?ReadView 是怎么画出来的?下一篇揭晓。
咖啡凉了,记得趁热喝。
评论 (0)