MySQL 如何实现 ACID

MySQL 如何实现 ACID

通俗版,适合快速理解底层机制


什么是 ACID

特性 含义 一句话
Atomicity(原子性) 事务要么全做,要么全不做 "转账扣钱和加钱必须一起成功"
Consistency(一致性) 事务前后数据满足所有约束 "余额不能变负数"
Isolation(隔离性) 并发事务互不干扰 "你转账时别人看不到中间状态"
Durability(持久性) 提交后数据不丢 "断电重启钱还在"

逐项实现机制(InnoDB 引擎)

1. 原子性 --- Undo Log

核心思路:做之前先记"怎么撤销"。

bash 复制代码
事务开始
  → INSERT 一行 → undo log 记录 "DELETE 这行"
  → UPDATE 余额 1000→800 → undo log 记录 "改回 1000"
事务失败/回滚
  → 按 undo log 逆序执行撤销操作
  • Undo log 存在独立的回滚段(rollback segment)
  • 事务提交后 undo log 不会立即删除(MVCC 还要用)

2. 持久性 --- Redo Log(WAL)

核心思路:先写日志,再写数据页。日志落盘 = 数据安全。

perl 复制代码
写入流程:
  修改内存中的数据页(Buffer Pool)
  → 同时写 redo log 到磁盘(顺序写,很快)
  → 返回"提交成功"
  → 后台异步把脏页刷到数据文件(随机写,慢)

崩溃恢复:
  启动时扫描 redo log
  → 已提交但未刷盘的事务 → 重放(redo)
  → 未提交的事务 → 用 undo log 回滚

关键配置innodb_flush_log_at_trx_commit

  • =1(默认):每次提交都 fsync,最安全
  • =2:写 OS 缓存,OS 崩才丢
  • =0:每秒刷,最多丢 1 秒

3. 隔离性 --- MVCC + 锁

MVCC(多版本并发控制):读不加锁,靠版本链实现。

bash 复制代码
每行数据隐藏两列:
  - trx_id:最后修改该行的事务 ID
  - roll_pointer:指向 undo log 中的旧版本

读取时:
  根据事务启动时的"快照"(Read View)判断:
  这行的 trx_id 在我启动前已提交?→ 可见
  没提交?→ 沿 roll_pointer 找更早的版本

锁机制(写-写冲突时):

  • 行锁(Record Lock):锁住具体行
  • 间隙锁(Gap Lock):锁住索引间隙,防幻读
  • 临键锁(Next-Key Lock)= 行锁 + 间隙锁

四种隔离级别

级别 脏读 不可重复读 幻读 MySQL 默认
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ ✗*
SERIALIZABLE

*InnoDB 在 RR 级别通过 Next-Key Lock 基本解决幻读(快照读靠 MVCC,当前读靠间隙锁)


4. 一致性 --- 以上三者 + 约束

一致性不是单独一个机制,而是结果

markdown 复制代码
一致性 = 原子性(不做一半)
       + 持久性(做完不丢)
       + 隔离性(并发不乱)
       + 数据库约束(NOT NULL / UNIQUE / FK / CHECK)
       + 应用层逻辑正确

5. Binlog 与两阶段提交(2PC)

为什么需要两阶段提交?

MySQL 有两份日志记录数据变更:

日志 层级 类型 用途
redo log InnoDB 引擎层 物理日志(页号+偏移+数据) 崩溃恢复
binlog Server 层 逻辑日志(SQL/行变更) 主从复制、备份恢复

问题:如果两份日志写入不一致,会出现主从数据不一致。

ini 复制代码
假设不用 2PC,先写 redo 再写 binlog:

场景:UPDATE balance SET amount=800 WHERE id=1  (原值 1000)

  ① redo log 写入(amount=800)  ✓
  ② ------断电------
  ③ binlog 未写入              ✗

结果:
  主库恢复后 amount=800(redo 重放)
  从库 amount=1000(binlog 没收到这条)
  → 主从不一致!

两阶段提交流程

perl 复制代码
┌─────────────────────────────────────────────────────┐
│                    事务提交流程                        │
├─────────────────────────────────────────────────────┤
│                                                      │
│  Phase 1: PREPARE                                    │
│  ┌──────────────────────────────────┐               │
│  │ redo log 写入磁盘,标记 prepare   │               │
│  │ (此时 redo log 状态 = prepare)  │               │
│  └──────────────────────────────────┘               │
│                    │                                  │
│                    ▼                                  │
│  Phase 2: COMMIT                                     │
│  ┌──────────────────────────────────┐               │
│  │ ① binlog 写入磁盘(fsync)       │               │
│  │ ② redo log 标记 commit           │               │
│  └──────────────────────────────────┘               │
│                                                      │
└─────────────────────────────────────────────────────┘

关键点:binlog 写入成功 = 事务提交的判定标准。


6. 断电崩溃恢复 --- MySQL 如何保障 ACID

恢复流程总览

perl 复制代码
MySQL 启动
  → 扫描 redo log 中所有 prepare 状态的事务
  → 逐个检查对应 binlog 是否完整写入
  → 决定提交还是回滚

五种断电时刻的处理

perl 复制代码
时间线:
  ─────①────②────③────④────⑤────→
       │    │    │    │    │
       │    │    │    │    └─ commit 标记后(已完成,无影响)
       │    │    │    └─ binlog fsync 后,redo 标记 commit 前
       │    │    └─ binlog 写入中(不完整)
       │    └─ redo prepare 后,binlog 写入前
       └─ redo 写入前(事务执行中)
断电时刻 redo log 状态 binlog 状态 恢复动作 数据结果
① 执行中 无/不完整 什么都不做 事务未发生
② prepare 后 prepare 回滚(undo log) 事务撤销
③ binlog 写入中 prepare 不完整 回滚(undo log) 事务撤销
④ binlog 已 fsync prepare 完整 提交(标记 commit) 事务生效
⑤ 全部完成 commit 完整 无需处理 事务生效

判定逻辑伪代码

sql 复制代码
for each redo_log_entry where status == PREPARE:
    xid = redo_log_entry.transaction_id
    if binlog contains complete entry for xid:
        # binlog 完整 → 事务已对外可见(从库可能已复制)
        → COMMIT(重放 redo,标记 commit)
    else:
        # binlog 不完整或不存在 → 事务未对外承诺
        → ROLLBACK(用 undo log 撤销)

为什么 binlog 完整就必须提交?

复制代码
因为 binlog 一旦 fsync 到磁盘:
  → 从库可能已经拉取并执行了这条变更
  → 如果主库回滚,主从就不一致了
  → 所以:binlog 落盘 = 不可反悔的承诺

各组件在崩溃恢复中的角色

arduino 复制代码
┌────────────┬────────────────────────────────┐
│ 组件        │ 崩溃恢复中的作用                │
├────────────┼────────────────────────────────┤
│ Redo Log   │ 重放已提交事务的数据页修改       │
│ Undo Log   │ 回滚未提交事务的修改             │
│ Binlog     │ 作为"事务是否已提交"的仲裁者     │
│ Checkpoint │ 标记已刷盘位置,缩小扫描范围     │
│ Doublewrite│ 防止部分页写入(torn page)      │
└────────────┴────────────────────────────────┘

Doublewrite Buffer(补充:防半页写入)

arduino 复制代码
问题:InnoDB 页 16KB,OS 一次写 4KB,写到一半断电 → 页损坏

解决:
  刷脏页前 → 先把页写到 doublewrite buffer(顺序写,连续空间)
  doublewrite 落盘 → 再写到真正的数据文件位置

崩溃时:
  数据文件页损坏 → 从 doublewrite buffer 恢复完整页
  然后再用 redo log 重放

一图总结

scss 复制代码
         ┌─────────────────────────────────────┐
         │           事务执行                    │
         └──────────────┬──────────────────────┘
                        │
         ┌──────────────┼──────────────────────┐
         │              │                       │
    Undo Log       Redo Log (WAL)         MVCC + 锁
    (原子性)        (持久性)              (隔离性)
         │              │                       │
         └──────────────┼──────────────────────┘
                        │
                 两阶段提交 (2PC)
           ┌────────────┼────────────┐
           │                         │
      Redo prepare              Binlog fsync
           │                         │
           └────────────┬────────────┘
                        │
                  Redo commit
                        │
              ┌─────────┴─────────┐
              │   崩溃恢复决策      │
              │ binlog 完整→提交    │
              │ binlog 缺失→回滚    │
              └───────────────────┘
                        │
                   一致性(结果)

常见面试追问

问题 答案要点
redo log 和 binlog 区别? redo 是引擎层物理日志(页+偏移),binlog 是 Server 层逻辑日志(SQL/行变更)
两阶段提交是什么? prepare(redo) → write(binlog) → commit(redo),保证两份日志一致
MVCC 能完全替代锁吗? 不能,写-写冲突必须加锁;SELECT ... FOR UPDATE 也走锁
undo log 什么时候清理? purge 线程判断没有 Read View 引用旧版本时清理

生成时间:2026-08-07

相关推荐
GitLqr4 小时前
Flutter 实战:为你的 App 增加桌面快捷方式 (Quick Actions)
flutter·app·全栈
烬羽21 小时前
上来就用 useState?你大概率用错了——useRef 的三种正确打开方式
react.js·前端框架·全栈
烬羽1 天前
《React Router 受保护路由的 3 个坑,第 2 个 90% 的人都踩过》
react.js·性能优化·全栈
用户938515635071 天前
React Router 进阶:路由守卫、登录鉴权与状态传递
前端·javascript·全栈
GitLqr1 天前
iOS 27 强制要求 UISceneDelegate:UIKit 和 Flutter 开发者该如何应对?
flutter·ios·全栈
烬羽2 天前
从"整个页面刷新"到"丝滑切换":手写一个 HashRouter 彻底搞懂前端路由
javascript·前端框架·全栈
用户938515635072 天前
从零理解 React Router v6:每一个 API 都是怎么工作的
前端·javascript·全栈
用户938515635072 天前
从"坐电梯"到"前端路由"——深入理解 Hash 路由原理
前端·typescript·全栈
用户938515635072 天前
React 组件设计的三个层次:从类型约束到状态归属,再到纯展示
typescript·全栈