Skip to content

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 没收到?不可能
Cbalance >= 0 约束保证不会透支;A+B 总余额不变
I其他事务在转账过程中看到的 balance 要么是转账前,要么是转账后
DCOMMIT 之后,即使断电,转账结果也不会丢失

三、图解全景

             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与事务索引**

Knowledge4J — Java 知识库