03.3 - 事务 ACID 实现
定位:ACID 是事务的四大保证——理解 InnoDB 如何通过 Redo Log、Undo Log、锁、MVCC 等机制分别保证这四个特性 面试高频度:⭐⭐⭐⭐ 考查方式:ACID 分别如何实现、Redo Log 保证什么、Undo Log 保证什么、"一致性和其他三者的关系"
一、这是什么?为什么需要它?
是什么
ACID 是数据库事务的四个基本特性:
| 特性 | 含义 | 一句话 |
|---|---|---|
| Atomicity(原子性) | 事务要么全部成功,要么全部回滚 | "全做或全不做" |
| Consistency(一致性) | 事务前后,数据满足所有约束 | "从一个正确状态到另一个正确状态" |
| Isolation(隔离性) | 并发事务互不干扰 | "像在单独运行一样" |
| Durability(持久性) | 提交的数据不会丢失 | "写入就不会丢" |
为什么需要 ACID?
没有 ACID 的数据库:
- 转账扣了钱但对方没收到(原子性缺失)
- 一个事务读到另一个事务未提交的数据然后决策错误(隔离性缺失)
- 数据库刚提交完突然断电,数据没了(持久性缺失)
- 唯一约束突然多出两条相同记录(一致性缺失)
ACID 的价值:让开发者可以假设数据库是可靠的——不需要在应用层处理部分失败、并发冲突、数据丢失等问题。
核心 insight:ACID 不是"四个独立特性",而是一层套一层的保证——A、I、D 是实现手段,C 是最终目标。或者说:A+I+D → C。
二、原理拆解
2.1 ACID 实现总览图
┌─────────────────────────────────────┐
│ ACID │
│ │
│ ┌──────────────────────────────┐ │
│ │ 一致性 (Consistency) │ │
│ │ = A + I + D + 应用逻辑 │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌────────┼────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │原子性│ │隔离性│ │持久性│ │
│ │Undo │ │锁 + │ │Redo │ │
│ │Log │ │MVCC │ │Log │ │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────┘2.2 原子性(Atomicity)—— Undo Log 保证
原子性 = 一个事务内的操作要么全部成功,要么全部撤销。
Undo Log 的回滚机制
事务 BEGIN:
┌─────────────────────────────────┐
│ INSERT INTO users ... │
│ → Undo Log 记录:删除该行 │
│ │
│ UPDATE users SET age=30 ... │
│ → Undo Log 记录:改回 age=20 │
│ │
│ DELETE FROM users WHERE id=1 │
│ → Undo Log 记录:重新插入该行 │
│ │
│ ... (更多操作) │
└─────────────────────────────────┘
│
├── COMMIT: Undo Log 标记为"不再需要"
│ (后续由 Purge 线程清理)
│
└── ROLLBACK: 从后往前读取 Undo Log
逐条执行"逆向操作"
恢复到事务开始前的状态回滚执行过程:
原始数据: id=1, name='Tom', age=20
事务 T1:
1. UPDATE users SET age=30 WHERE id=1
→ Undo Log: 记录 (id=1) 的旧值 age=20
2. UPDATE users SET name='Jerry' WHERE id=1
→ Undo Log: 记录 (id=1) 的旧值 name='Tom'
3. 执行失败 → ROLLBACK
→ 从后往前回滚:
步骤 2 回滚: name='Tom'
步骤 1 回滚: age=20
→ 数据恢复到: id=1, name='Tom', age=20 ✅原子性与 Redo 的关系
注意:只有 Undo Log 保证原子性吗?不,还需要 Redo Log 配合。
如果事务在 COMMIT 之前崩溃,恢复时用 Undo Log 回滚未完成的事务。 如果事务在 COMMIT 之后崩溃,恢复时用 Redo Log 重放已提交的事务。
2.3 隔离性(Isolation)—— 锁 + MVCC 保证
| 机制 | 解决什么问题 | 实现方式 |
|---|---|---|
| MVCC | 读不阻塞写、写不阻塞读 | 多版本可见性控制 |
| 行锁 | 写操作之间的互斥 | 在索引上对行加锁 |
| 间隙锁 | 幻读(当前读场景) | 对索引键之间的间隙加锁 |
| 意向锁 | 快速检测表级/行级锁冲突 | 表级别的"预告标志" |
不同隔离级别下的隔离性实现:
MVCC 行锁 间隙锁
RU ✗ ✗ ✗
RC 每条语句 ✅ ✗
RR 整个事务 ✅ ✅
SERIALIZABLE ✗ 全部加锁 全部加锁2.4 持久性(Durability)—— Redo Log 保证
持久性 = 一旦事务提交成功,数据就不会丢失。
Redo Log + WAL(Write-Ahead Logging)
事务提交过程:
1. 修改 Buffer Pool 中的数据页(内存)
2. 将修改记录写入 Redo Log Buffer(内存)
3. Redo Log Buffer 刷到 Redo Log 文件(磁盘)← **WAL: 先写日志**
4. 事务提交成功 ✅
5. 后台线程将脏页刷到数据文件(磁盘)← **异步刷盘**
为什么先写日志比直接写数据快?
数据页写入: 随机写(16KB 页在不同位置)
Redo Log 写入: 顺序写(追加到文件末尾)
顺序写比随机写快 10-100 倍!崩溃恢复流程:
MySQL 崩溃重启后:
┌─────────────────────────────────────────────┐
│ 1. 扫描 Redo Log │
│ → 找到所有已提交但未写入数据文件的修改 │
│ │
│ 2. Redo(重放): │
│ 把 Redo Log 中的修改重新应用到数据页 │
│ → 保证持久性 (D) │
│ │
│ 3. Undo(回滚): │
│ 扫描 Undo Log,回滚所有未提交的事务 │
│ → 保证原子性 (A) │
│ │
│ 结果:已提交的不会丢,未提交的全部撤销 │
└─────────────────────────────────────────────┘持久性的配置
ini
# 持久性相关配置
innodb_flush_log_at_trx_commit = 1 # 最安全,但最慢
# 1: 每次事务提交都刷 Redo Log 到磁盘(保证持久性)
# 2: 每次事务提交只写到 OS cache,每秒刷盘(OS 崩溃可能丢 1s 数据)
# 0: 每秒刷盘一次(MySQL 崩溃可能丢 1s 数据)
sync_binlog = 1 # 每次事务提交都刷 Binlog性能与安全权衡:
innodb_flush_log_at_trx_commit | 安全性 | 性能 |
|---|---|---|
| 1 | ✅ 完整持久性(不丢数据) | 低(每秒事务数受限) |
| 2 | ⚠️ OS 崩溃可能丢 1s | 中 |
| 0 | ⚠️ MySQL 崩溃可能丢 1s | 高 |
2.5 一致性(Consistency)—— A + I + D + 应用逻辑
一致性是 A、I、D 共同作用的结果,加上应用层自身的约束保证。
C(一致性) ← 最终目标
│
├── A(原子性)保证:事务不会"半完成"
│ └─ 通过 Undo Log → 要么全做要么全不做
│
├── I(隔离性)保证:并发事务不会互相干扰
│ └─ 通过 MVCC + 锁 → 每个事务看到一致的数据视图
│
├── D(持久性)保证:已提交的数据不会丢失
│ └─ 通过 Redo Log → 崩溃后恢复
│
└── 应用层保证:
├─ 约束(PRIMARY KEY, UNIQUE, FOREIGN KEY)
├─ 触发器(Trigger)
└─ 业务逻辑(应用代码)例子——转账:
sql
-- A 账户转 100 元给 B 账户
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;| ACID 特性 | 在这个转账中的作用 |
|---|---|
| A | 如果任一 UPDATE 失败,全部回滚——A 扣了钱但 B 没收到?不可能 |
| C | balance >= 0 约束保证不会透支;A+B 总余额不变 |
| I | 其他事务在转账过程中看到的 balance 要么是转账前,要么是转账后 |
| D | COMMIT 之后,即使断电,转账结果也不会丢失 |
三、图解全景
InnoDB ACID 实现架构图
┌─────────────────────────────────────────────────────────┐
│ 事务 ACID 实现 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 原子性 (Atomicity) │ │
│ │ ┌────────────────┐ ┌────────────────────┐ │ │
│ │ │ ROLLBACK 时 │ │ 崩溃恢复时 │ │ │
│ │ │ Undo Log 逆向 │ │ Undo Log 回滚 │ │ │
│ │ │ 操作 → 恢复初始 │ │ 未提交的事务 │ │ │
│ │ └────────────────┘ └────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 持久性 (Durability) │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ WAL: 先写 Redo Log 后写 │ │ │
│ │ │ 数据文件 │ │ │
│ │ │ 崩溃恢复:Redo Log 重放 │ │ │
│ │ │ 未刷盘的已提交修改 │ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 隔离性 (Isolation) │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ MVCC │ │ 行锁 │ │ 间隙锁 │ │意向锁 │ │ │
│ │ │快照读 │ │行级互斥│ │防幻读 │ │表级预告│ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 一致性 (Consistency) ← A + I + D + 约束 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 约束条件 │ │ 触发器 │ │ 应用逻辑 │ │ │
│ │ │ PK/UK/FK │ │ │ │ 代码保证 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘四、实战验证
验证原子性——回滚
sql
-- 测试表
CREATE TABLE test_acid (
id INT PRIMARY KEY,
val INT
);
-- 测试原子性
START TRANSACTION;
INSERT INTO test_acid VALUES (1, 100);
INSERT INTO test_acid VALUES (1, 200); -- 主键冲突!
-- 注意:某些引擎会部分提交前面的语句
-- InnoDB 会回滚整个事务
ROLLBACK; -- 或者 MySQL 自动回滚
SELECT * FROM test_acid; -- 无数据 ✅ 原子性保证了验证持久性——WAL 效果
sql
-- 查看 Redo Log 配置
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW VARIABLES LIKE 'innodb_log_file_size';
SHOW VARIABLES LIKE 'innodb_log_files_in_group';
-- 查看 Redo Log 使用情况
SHOW ENGINE INNODB STATUS\G
-- 看 LOG 部分
--
-- Log sequence number ← 当前写入位置
-- Log flushed up to ← 已刷盘位置
-- Last checkpoint at ← 最近检查点位置验证持久性——配置对比
sql
-- 测试不同持久化配置下的写入性能
-- 配置 1: 最安全
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
-- 测试: 10万次事务提交,每次刷盘
-- 预期: 约 500-1000 tps
-- 配置 2: 折中
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
-- 测试: 10万次事务提交,不强制同步刷盘
-- 预期: 约 3000-5000 tps
-- 配置 3: 性能最高
SET GLOBAL innodb_flush_log_at_trx_commit = 0;
-- 测试: 10万次事务提交,每秒刷一次
-- 预期: 约 5000-10000 tps验证隔离性
sql
-- 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- MySQL 8.0: REPEATABLE-READ(默认)
-- 修改隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;五、面试视角
| 追问 | 答案要点 |
|---|---|
| ACID 分别由什么保证? | A=Undo Log, I=MVCC+锁, D=Redo Log, C=A+I+D+约束 |
| Redo Log 和 Undo Log 各保证什么? | Redo → 持久性(已提交不丢),Undo → 原子性(未提交能回滚) |
| 一致性和 AID 的关系? | C 是目标,A+I+D 是实现手段。没有 AID 就没有 C |
innodb_flush_log_at_trx_commit=1 和 =2 的区别? | 1=每次提交刷磁盘(满持久性),2=写 OS cache 每秒刷(OS 崩溃可能丢数据) |
| 崩溃恢复过程? | Redo 已提交未刷盘的数据,Undo 未提交的数据(先 Redo 再 Undo) |
| 为什么 WAL 快? | Redo Log 是顺序追加写,数据文件是随机写。顺序写比随机写快 10-100 倍 |
📚 相关链接
- MVCC:**MVCC实现原理** — 隔离性的核心实现
- 隔离级别:**事务隔离级别** — 不同隔离级别对比
- Redo Log:**Redo Log 详解**
- Undo Log:**Undo Log 详解**
- ← 返回 **SQL与事务索引**