Skip to content

02 · 事务与 ACID ​

事务这块最容易背成四个字母。真正的问题是:数据库凭什么敢说"这条数据一定不会丢"?

一句话本质 ​

事务把"多条操作"打包成"一个不可分割的整体":要么全做,要么全不做,且做完就一定能查到。

生活类比:银行转账 ​

A 给 B 转 100 元 = 两个动作:
  ① A 的余额 -100
  ② B 的余额 +100

如果 ① 做完系统崩了:
  没有事务 > A 少了 100,B 没收到,钱凭空消失
  有事务   > ① 被撤销(undo),或者 ② 被补做(redo)

核心机制 ​

一、ACID 到底谁负责 ​

字母含义靠什么实现
A 原子性全做或全不做undo log(记录旧值,回滚时反向操作)
C 一致性数据满足约束(余额不能为负、外键有效)不是数据库单独实现的,是 A+I+D + 应用逻辑共同保证的结果
I 隔离性并发事务互不干扰锁 + MVCC(见 03、04 篇)
D 持久性提交后即使断电也不丢redo log(WAL,先写日志再写数据)

最重要的认知:C 是目标,A、I、D 是手段。

面试问"ACID 怎么实现",答"A 靠 undo、D 靠 redo、I 靠锁和 MVCC、C 是前三者加上业务约束共同保证的结果"——这个层次就对了。如果答"C 靠……",说明没理解。

二、undo log:怎么撤销 ​

执行 UPDATE user SET balance = 900 WHERE id = 1;  (原值 1000)

undo log 记录:
  "id=1 的 balance 原来是 1000"

回滚时:UPDATE user SET balance = 1000 WHERE id = 1

undo log 还兼职做 MVCC:它保存了旧版本,所以其他事务能读到"修改前的值"。这就是 03 篇版本链的来源。

undo log 是逻辑日志:记录"做了什么操作",回滚时执行相反操作。不是物理的"页变成了什么样"。

三、redo log:怎么保证不丢 ​

核心问题:修改数据要先改 Buffer Pool(内存),再刷到磁盘。但如果每次提交都刷盘,太慢(一次随机 IO 10ms)。

WAL(Write-Ahead Logging)的做法:

① 修改 Buffer Pool(内存里的页)
② 把"改了什么"追加到 redo log(顺序写,很快)
③ redo log 刷盘(fsync)> 此时就可以回复客户端"提交成功"
④ 数据页在后台慢慢刷盘(异步,不阻塞)

如果断电:
  Buffer Pool 里的修改丢了,但 redo log 已经落盘
  重启时重放 redo log,把丢失的修改补回来

为什么快:顺序写 vs 随机写。

  • 直接改数据页:要找到那一页在磁盘的位置,随机写(10ms 级)
  • 写 redo log:追加到文件末尾,顺序写(0.1ms 级)

关键参数 innodb_flush_log_at_trx_commit:

值行为丢失风险
1(默认)每次提交都 fsync不丢(最安全)
0每秒刷一次丢 1 秒
2写到 OS 缓存,每秒刷丢 1 秒(OS 崩)/ 不丢(只是 MySQL 崩)

四、binlog:Server 层的日志 ​

redo logbinlog
层级InnoDB 引擎层Server 层
内容物理日志("某页某偏移改成了什么")逻辑日志("执行了哪条 SQL")
写入循环写(固定大小,覆盖旧内容)追加写(不覆盖)
用途崩溃恢复主从复制、数据恢复

为什么两个都要:

  • redo log 是 InnoDB 自己的,循环写,只能用于崩溃恢复。
  • binlog 是 Server 层的,所有引擎共用,追加写,能用于主从同步和按时间点恢复。

五、两阶段提交(这是最常考的一致性细节) ​

问题:一次提交要写两个日志(redo 和 binlog),怎么保证两个都成功?

如果先写 redo 再写 binlog,写完 redo 后崩了:
  > redo 说"已提交",binlog 没有
  > 主库有这条数据,从库(靠 binlog 同步)没有 > 主从不一致

如果先写 binlog 再写 redo,写完 binlog 后崩了:
  > binlog 有,主库没有 > 从库多了一条数据 > 不一致

解法:两阶段提交

① 写 redo log,标记为 prepare 状态
② 写 binlog
③ 写 redo log,标记为 commit 状态

崩溃恢复时的判断:
  - redo 是 prepare,binlog 完整 > 提交(步骤 ③ 没做完,补做)
  - redo 是 prepare,binlog 不完整 > 回滚

关键:用 binlog 是否完整来决定 redo 该提交还是回滚。这样两个日志要么都生效,要么都不生效。

六、事务的隔离性与异常(为 03 篇铺垫) ​

异常现象
脏读读到了别的事务未提交的数据(对方可能回滚)
不可重复读同一事务内,两次读同一行,结果不同(别人改了并提交)
幻读同一事务内,两次范围查询,行数不同(别人插入并提交)

不可重复读 vs 幻读的区别:

  • 不可重复读:同一行的值变了(UPDATE)
  • 幻读:行数变了(INSERT/DELETE)

四个隔离级别(详见 03 篇):

级别脏读不可重复读幻读
读未提交✅ 可能✅✅
读已提交❌✅✅
可重复读(MySQL 默认)❌❌⚠️ 理论上可能,InnoDB 用间隙锁基本解决
串行化❌❌❌

为什么这么设计 ​

反事实 1:如果没有 undo log ​

事务执行到一半失败,无法回滚。数据处于"一半改了、一半没改"的中间状态,业务逻辑完全错乱。

而且没有 undo log 就没有 MVCC——因为 MVCC 要靠 undo log 里的旧版本。

反事实 2:如果没有 redo log(每次直接刷数据页) ​

每次提交都要随机写磁盘。一个事务改 10 行、分布在 5 个页上 > 5 次随机 IO = 50ms。TPS 只有 20。

有了 redo log(顺序写):一次追加 ≈ 0.1ms。TPS 能到几千。

redo log 的本质是"把随机写变成顺序写"——这是数据库性能的核心机密。

反事实 3:如果没有两阶段提交 ​

主从数据不一致。而且这种不一致是静默的——不会报错,只在某天发现"从库少了 3 条数据",且无法追溯是哪三条。

面试怎么问 ​

Q:ACID 分别怎么实现?

A > undo log(记录旧值,反向操作回滚) D > redo log(WAL,先写日志再刷数据页) I > 锁 + MVCC C > 不是单独实现的,是 A/I/D 加上业务约束共同保证的结果 这个" C 不是数据库实现的"是加分点。

Q:redo log 和 binlog 的区别?

层级(引擎层 vs Server 层)、内容(物理 vs 逻辑)、写入方式(循环写 vs 追加写)、用途(崩溃恢复 vs 主从复制)。

Q:为什么需要两阶段提交?

因为要写两个日志,必须保证原子性。用"redo prepare > binlog > redo commit",崩溃时根据 binlog 是否完整决定提交还是回滚。

Q:WAL 是什么?为什么快?

Write-Ahead Logging:先写日志再写数据页。快的原因是顺序写替代随机写——日志追加到文件末尾,而数据页要定位到磁盘随机位置。

追问链:

事务怎么保证原子性?
  > undo log 记了什么?(旧值)
    > 那持久性呢?(redo log)
      > redo log 为什么比直接改数据页快?(顺序写 vs 随机写)
        > 那 binlog 呢?(Server 层,用于主从)
          > 两个日志怎么保证一致?(两阶段提交)
            > 崩溃时怎么判断?(redo 是 prepare 且 binlog 完整 > 提交)

和其他知识的连接 ​

自测 3 问 ​

Q1:为什么说"一致性(C)不是数据库实现的"?

点开看答案

因为一致性是业务层面的目标,不是数据库能单独保证的。

数据库能保证的:

  • 原子性(要么全做要么全不做)
  • 隔离性(并发不互相干扰)
  • 持久性(提交了就不丢)

但"数据符合业务规则"这件事,数据库管不了。比如:

  • "账户余额不能为负"——数据库不知道这个规则,除非你写 CHECK 约束或触发器。
  • "A 转账给 B,总额不变"——这需要应用层保证两个 UPDATE 在同一个事务里。

准确说法:A、I、D 是数据库提供的手段,C 是这些手段加上业务约束共同达成的结果。

面试这样答,比背"一致性由原子性隔离性持久性共同保证"更清楚。

Q2:redo log 循环写,写满了怎么办?

点开看答案

redo log 是固定大小的环形文件(比如 4 个 1GB 的文件),有一个"写指针"和一个"检查点指针"。

  • 写指针往前走,记录新的日志。
  • 检查点指针表示"这个位置之前的日志对应的数据页,已经刷到磁盘了,可以覆盖"。

写满时:写指针追上检查点指针 > 必须停下来,强制把 Buffer Pool 里的脏页刷盘,推进检查点,腾出空间。

这就是为什么"redo log 太小会导致性能抖动"——频繁触发强制刷盘,把异步刷盘变成了同步等待。

工程建议:redo log 不要设太小,让检查点推进有足够缓冲。

Q3:既然有 redo log,为什么还需要 binlog?

点开看答案

因为它们服务于不同的目的,不能互相替代:

redo logbinlog
谁需要只有 InnoDB 自己整个 MySQL Server(包括主从、备份工具)
能干什么崩溃恢复(重放未刷盘的修改)主从复制(从库重放主库的 binlog)、按时间点恢复
写入方式循环写,会被覆盖追加写,永久保留(直到手动清理)
内容物理(页级别的修改)逻辑(SQL 语句或行变更)

关键:redo log 会被覆盖,所以不能用来做主从同步(从库落后太多就丢了)。binlog 追加写且可长期保留,才能支撑"从库延迟几小时再同步"。

另外,redo log 是 InnoDB 特有的,如果换成 MyISAM 引擎就没有了——但 binlog 在 Server 层,所有引擎都有。

类比失效处 / 易错点 ​

  • "转账"类比会让人以为事务只用于多表操作。单表单行也能用事务,只要需要原子性。
  • undo log 不只是"回滚用"。它还承担 MVCC 的"旧版本"职责,所以长事务会导致 undo log 无法清理、版本链过长、磁盘暴涨。
  • redo log 不是"备份"。它是"还没刷到数据页的修改记录",一旦对应数据页刷盘,那部分 redo 就没用了(可以被覆盖)。
  • 两阶段提交(数据库内部)≠ 分布式事务的 2PC。虽然思想相似,但一个是"两个日志",一个是"多个节点"。面试时要说清是哪个。
  • 隔离级别越高越好是错的。串行化性能极差,实际业务几乎不用;MySQL 默认的可重复读已经能满足绝大多数场景。

✅ 验收清单 ​

  • 能说出 ACID 四个字母各由什么机制实现,并解释"为什么 C 不是数据库实现的"
  • 能说清 undo log 的两个作用(回滚 + MVCC 旧版本)
  • 能用"顺序写 vs 随机写"解释 WAL 为什么快
  • 能对比 redo log 和 binlog 的四个区别(层级、内容、写入、用途)
  • 能讲清两阶段提交的流程,以及崩溃时怎么判断该提交还是回滚
  • 能区分脏读、不可重复读、幻读,并说出各自的成因(UPDATE vs INSERT)
  • 能说出 redo log 写满时会发生什么
  • 合上书,用自己的话讲 3 分钟,中途不卡壳