---
title: "面试必问：ACID 你真的懂了吗？"
description: "结合示例讲解数据库事务 ACID 四大特性，深入分析 MySQL InnoDB 如何通过 Undo Log、Redo Log、锁机制与 MVCC 实现原子性、隔离性与持久性。"
date: 2025-02-19T07:45:55Z
canonical: https://xiaobox.github.io/p/2025-02-19-mian-shi-bi-wen-acid-ni-zhen-de-dong-le-ma/
author: 小盒子
categories: ["行业与思考"]
tags: ["MySQL", "面试", "多线程"]
source: https://mp.weixin.qq.com/s/3_O1u2P11h7Ng94Fyiqq4Q
license: CC BY-NC-SA 4.0
license_url: https://creativecommons.org/licenses/by-nc-sa/4.0/
---

# 面试必问：ACID 你真的懂了吗？

## ACID 是什么？

事务处理中的 ACID 是确保数据库操作可靠性和完整性的四个核心特性

| **属性** | **说明** | **示例** |
| --- | --- | --- |
| **原子性（Atomicity）** | *事务是不可分割的最小操作单元，事务中的所有操作要么全部成功完成，要么全部失败回滚。* | `用户在线购买书籍时的支付流程：`
 ①支付扣款 ②库存扣减 ③快递下单三个步骤必须全部成功——任一步骤失败时（如库存不足），系统自动取消已付金额，退回到购物未进行状态。 |
| **一致性（Consistency）** | *事务必须保证数据库从一个一致性状态转变到另一个一致性状态。一致性是指数据必须符合预定义的规则和约束，例如完整性约束、业务规则等。* | `银行转账场景：`
 账户A向账户B转200元后，两人账户总额保持不变（若A+B原为1000元，操作完成后仍为1000）。即便系统中途崩溃，恢复后也不会出现A扣200元但B未入账的金额"凭空消失"。 |
| **隔离性（Isolation）** | *多个事务并发执行时，每个事务都应该感觉不到其他事务的存在，就像在隔离的环境中执行一样。事务之间互相隔离，不会互相影响。* | `航班订座系统：`
 当乘客A和B同时选择最后一个座位，先完成支付者的订单立即锁定座位，另一用户将实时看到"无余票"提示——避免出现系统误判导致超售。 |
| **持久性（Durability）** | *一旦事务提交成功，对数据库的修改就应该是永久性的，即使系统发生崩溃或重启等意外情况，数据也不会丢失。* | `线上预约挂号确认：`
 用户成功提交预约后，即便医院服务器遭遇断电，重启后系统依然保留该条预约记录并发送确认短信，不会因突发意外丢失数据。 |

* * *

## MySQL 是如何保证 ACID 的？

MySQL 实现 ACID 特性主要依赖 日志系统（undo log 和 redo log）、锁机制 和 MVCC 多版本并发控制。下面是具体实现原理的详细分析：

### 一、Atomicity（原子性）

事务是不可分割的最小执行单位。原子性确保事务中的所有操作要么全部成功完成，要么全部失败回滚。不允许中间状态。MySQL 通过 **Undo Log + 事务回滚** 实现原子性：

当事务开始时，InnoDB 会记录事务修改前的数据（旧版本）到 `Undo Log` 中，用于事务回滚时恢复原始状态。

> `Undo Log` 记录结构包含：原始数据值、事务ID（trx\_id）、回滚指针（roll\_pointer）。

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-19-mian-shi-bi-wen-acid-ni-zhen-de-dong-le-ma/001-8d841891.png)

`Undo Log` 记录的是逻辑操作，例如 "删除第 10 行"，"将字段 'name' 从 'old' 更新为 'new'" 等。

举个简单例子：

```sql
-- 事务未提交时，其他事务通过Undo Log读取原始数据（MVCC）
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 此时Undo Log会记录balance的旧值（如200）
ROLLBACK; -- 使用Undo Log恢复数据

```

如果事务执行过程中发生错误或者用户显式执行 ROLLBACK，InnoDB 可以根据 `Undo Log` 中的记录将数据恢复到事务开始之前的状态，从而实现事务的回滚

* * *

### 二、Isolation（隔离性）

隔离性 (Isolation) 是 ACID 特性中的关键一环，它确保在多个事务并发执行时，每个事务都仿佛独立运行，互不干扰。 换句话说，一个事务的中间状态和操作不应该被其他并发事务感知到，从而避免数据混乱和不一致。 为了实现这种隔离效果，MySQL 的 InnoDB 存储引擎主要依赖于两大核心机制：**锁机制 (Locking)** 和 **多版本并发控制 (MVCC)**

#### 1. 锁机制

首先，**锁机制是最基础的隔离手段**。InnoDB 实现了多种锁类型，以适应不同的并发场景和隔离需求。 其中，`行级锁` 是 InnoDB 并发控制的核心，它允许事务仅锁定需要修改的数据行，最大程度地提高了并发度。 行级锁又细分为 `共享锁 (S 锁)` 和 `排他锁 (X 锁)`，前者允许多个事务同时读取同一行数据，而后者则保证在更新或删除数据时，只有一个事务可以独占该行。

除了行级锁，MySQL 还提供 `表级锁`，它会锁定整个表，虽然并发度较低，但在某些特定场景（如执行 LOCK TABLES 语句）下仍然适用。

为了更高效地管理锁，InnoDB 引入了 `意向锁 (Intention Locks)`，它在表级别上预先声明事务对行级锁的意图，从而优化锁的检查和兼容性。

此外，在 REPEATABLE READ 和 SERIALIZABLE 这两个较高的隔离级别下，为了解决幻读问题，InnoDB 还使用了 `间隙锁 (Gap Locking)`，它不仅锁定已存在的记录，还锁定索引记录之间的间隙，防止其他事务在该间隙中插入新记录，从而彻底避免幻读。

我们通过一个具体的例子来说明 InnoDB 的 间隙锁（Gap Locking） 如何解决幻读问题：

假设有一张表 `students`，存储学生信息，主键为 `id`，当前数据如下：

| id | name |
| --- | --- |
| 1 | Alice |
| 3 | Bob |
| 5 | Charlie |

现在有两个事务 **事务A** 和 **事务A**，操作顺序如下：

1.  **事务A** 执行范围查询并加锁：

    ```
    BEGIN;
    SELECT * FROM students WHERE id BETWEEN 1 AND 5 FOR UPDATE;
    -- 查询结果：id=1, 3, 5
    ```

    InnoDB 会为 `id` 索引加上 **Next-Key Lock**（行锁 + 间隙锁），锁定的范围包括：

-   `(-∞, 1]`
-   `(1, 3]`
-   `(3, 5]`
-   `(5, +∞)`
    （注：假设表中无其他数据）

3.  **事务B** 尝试插入新数据：

    ```
    BEGIN;
    INSERT INTO students (id, name) VALUES (2, 'David');  -- 尝试插入到间隙 (1,3)
    -- 或者
    INSERT INTO students (id, name) VALUES (4, 'Eve');    -- 尝试插入到间隙 (3,5)
    ```

    由于 **事务A** 的间隙锁锁定了 `(1,3)` 和 `(3,5)` 的间隙，**事务B** 的插入操作会被阻塞，直到 **事务A** 提交或回滚。

4.  **事务A** 提交：

    ```
    COMMIT;
    ```

    **事务B** 的插入操作才会继续执行。

**结果对比**

-   **没有间隙锁**：
    若事务A未加间隙锁（例如使用 `READ COMMITTED` 隔离级别），事务B可以插入 `id=2` 或 `id=4`。当事务A再次执行 `SELECT` 时，会看到新插入的行（id=2 或 4），导致**幻读**。

-   **有间隙锁**：
    事务B的插入操作被阻塞，直到事务A释放锁。事务A在事务执行期间始终看到相同的数据（id=1,3,5），**避免幻读**。

### **关键点**

1.  **间隙锁的锁定范围**：
    InnoDB 的间隙锁不仅锁定已存在的行，还会锁定索引记录之间的“间隙”（例如 `(1,3)` 和 `(3,5)`），阻止其他事务在间隙中插入新数据。

2.  **Next-Key Lock 的作用**：
    Next-Key Lock = 行锁（锁定已存在记录） + 间隙锁（锁定间隙）。例如，对 `id=3` 的行锁会锁定范围 `(1,3]`。

3.  **隔离级别的影响**：
    间隙锁仅在 `REPEATABLE READ` 隔离级别下生效。在 `READ COMMITTED` 级别下，InnoDB 会禁用间隙锁，幻读仍可能发生。

在实际场景中，比如在电商系统中，若一个事务正在统计某商品（例如库存范围在 100~200）的订单数量，间隙锁可以防止其他事务插入新的订单记录（例如库存为 150 的商品），确保统计结果的一致性。

#### 2. MVCC

为了进一步提升并发性能，尤其是在读多写少的场景下，InnoDB 引入了 `多版本并发控制 (MVCC)`。 **MVCC 的核心思想是允许事务在读取数据时，访问数据在某个时间点的快照版本，而不是直接读取最新的数据。** 这样，读操作就不需要等待写操作完成，从而实现读写并发执行，显著提高了系统吞吐量。  MVCC 的实现依赖于 `Undo Log` 和 `Read View (快照读)`。 `Undo Log` 用于记录数据的历史版本，而 `Read View` 则定义了事务在读取数据时应该看到哪个版本的数据。MVCC 主要应用于 READ COMMITTED 和 REPEATABLE READ 这两个隔离级别，在这两个级别下，MVCC 可以有效减少锁的竞争，提升并发性能。

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-19-mian-shi-bi-wen-acid-ni-zhen-de-dong-le-ma/002-9516a24c.png)

总结来说：MVCC 就是基于隐藏字段、undo\_log 链和 ReadView 来实现的

#### 3. 隔离级别与策略对比

最后，为了满足不同应用场景对隔离程度和性能的不同需求，MySQL 提供了 四种事务隔离级别。 从最低的 READ UNCOMMITTED (读未提交) 到最高的 SERIALIZABLE (串行化)，隔离级别依次增强，但并发性能也随之降低。

-   READ UNCOMMITTED 允许脏读，隔离性最弱，但性能最高；
-   READ COMMITTED 避免了脏读，但可能出现不可重复读；
-   REPEATABLE READ (**InnoDB 默认级别**) 在 READ COMMITTED 的基础上解决了不可重复读，但仍可能存在幻读（在某些情况下，InnoDB 通过 Next-Key Locking 尝试解决幻读）；
-   SERIALIZABLE 通过强制事务串行执行，彻底避免了所有并发问题，但并发性能也最低。

| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
| --- | --- | --- | --- | --- |
| READ UNCOMMITTED | ✔️ | ✔️ | ✔️ | 无锁 |
| READ COMMITTED | ✖️ | ✔️ | ✔️ | 每个SELECT生成新Read View |
| REPEATABLE READ\* | ✖️ | ✖️ | ✖️△ | 首SELECT生成Read View + 间隙锁 |
| SERIALIZABLE | ✖️ | ✖️ | ✖️ | 所有SELECT隐式转成`SELECT ... FOR UPDATE` |

> △：MySQL通过Next-Key Lock（行锁+间隙锁组合）在REPEATABLE READ级别实际消除幻读。

* * *

### 三、Durability（持久性）

持久性 (Durability) 是 ACID 特性中保障数据安全性的最后一道防线。 它确保一旦事务成功提交，对数据库所做的所有更改都必须被永久地保存下来，即使系统随后发生崩溃、断电或任何其他类型的故障，已提交的数据也绝不会丢失。  为了实现这种强大的数据保障，MySQL 的 InnoDB 存储引擎采用了一系列精密的机制，其中最核心的是 `Redo Log` (重做日志)，并辅以 `Write-Ahead Logging (WAL)` 策略、 `Doublewrite Buffer` (双写缓冲区) 和灵活的 刷盘 (Flush to Disk) 机制，同时，`Binlog` (二进制日志) 也从更广泛的层面为数据持久性提供了支持。

首先，Redo Log 是 InnoDB 实现持久性的基石。 当一个事务执行过程中，InnoDB 并不会立即将数据页的修改直接写入磁盘上的数据文件，而是先将这些修改操作，例如插入、更新或删除的具体内容，以一种紧凑、高效的形式，顺序地记录到 Redo Log Buffer 中。  这里的 Redo Log 记录的是物理层面的修改，例如“将数据页 X 的偏移量 Y 处的 Z 个字节修改为新的值”。  为了保证效率，Redo Log Buffer 存在于内存中，但为了确保持久性，InnoDB 会定期或者在事务提交时，将 Redo Log Buffer 中的内容刷新到 Redo Log 文件 这一磁盘上的持久化存储。

为了进一步确保数据在极端情况下的安全性，InnoDB 遵循 `Write-Ahead Logging` (WAL) 预写式日志 策略。  这意味着，在任何数据页的实际修改被写入磁盘数据文件之前，必须先将相应的 Redo Log 记录落盘到 Redo Log 文件中。  **这种 “先写日志，后写数据” 的机制至关重要，它保证了即使在数据页尚未完全刷入磁盘时系统发生崩溃，已经提交的事务的所有修改操作也已经安全地记录在 Redo Log 中，从而为后续的数据恢复提供了保障。**

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-19-mian-shi-bi-wen-acid-ni-zhen-de-dong-le-ma/003-3900f38d.png)

`Doublewrite Buffer` (双写缓冲区) 是 InnoDB 为了应对数据页“部分写失效 (Partial Write)” 问题而引入的增强机制。  在数据页从内存刷新到磁盘数据文件的过程中，可能会因为断电等意外情况，导致数据页只写入了一部分，造成数据损坏。  为了避免这种情况，InnoDB 在数据页最终写入数据文件之前，会先将其完整地写入 Doublewrite Buffer 区域。  Doublewrite Buffer 是磁盘上一个连续的存储区域，InnoDB 会先顺序写入，保证写入的原子性。  之后，再将数据页从 Doublewrite Buffer 拷贝到真正的数据文件位置。  这样，即使在数据页写入过程中发生崩溃，InnoDB 在重启恢复时，可以通过 Doublewrite Buffer 检查数据页的完整性。  如果发现数据页写入不完整或已损坏，可以从 Doublewrite Buffer 中找到该数据页的完整副本进行恢复，从而有效地避免了数据页部分写入导致的数据丢失。

`Flush to Disk` 机制 则提供了对 Redo Log 和数据页刷盘行为的精细控制。  MySQL 提供了多个参数，例如 innodb\_flush\_log\_at\_trx\_commit 参数控制 Redo Log 何时刷盘，可以设置为每次事务提交都刷盘 (最安全，但性能较低)，或者定期刷盘 (性能较高，但可能在崩溃时丢失少量已提交事务)。  innodb\_flush\_method 参数则控制数据页刷盘的具体方式，例如是否绕过操作系统缓存直接写入磁盘，以满足不同的性能和可靠性需求。  通过调整这些刷盘策略，用户可以在数据安全性和性能之间进行权衡，根据实际业务场景选择合适的配置。

![Image](https://pub-f29bf2b53160470c9a85250116509a24.r2.dev/post/2025-02-19-mian-shi-bi-wen-acid-ni-zhen-de-dong-le-ma/004-7797e2c0.png)

最后，虽然 Binlog (二进制日志) 的主要用途是用于数据库的主从复制和时间点恢复，但它也间接地为数据持久性做出了贡献。  Binlog 记录了数据库中所有的数据变更操作 (逻辑操作，例如 SQL 语句)，这些日志可以用于数据库的备份和恢复，特别是当需要进行全量或增量备份，或者需要恢复到某个特定的时间点时，Binlog 就显得至关重要。  虽然 Binlog 的关注点和 Redo Log 略有不同 (Redo Log 侧重于崩溃恢复，Binlog 侧重于时间点恢复和复制)，但它们都为确保数据的长期安全性和可恢复性提供了重要的支持。

总结来说：**MySQL InnoDB 通过 Redo Log + WAL 策略 保障事务提交的修改能够被可靠地记录下来， Doublewrite Buffer 增强了数据页写入的可靠性，Flush to Disk 机制 提供了灵活的刷盘控制，而 Binlog 则从更广泛的层面支持数据备份和时间点恢复**。 这些机制相互配合，共同构建了持久性保障体系。

* * *

### 四、Consistency（一致性）

事务必须保证数据库从一个一致性状态转变到另一个一致性状态。一致性是指数据库的完整性约束没有被破坏。例如，主键唯一性、外键约束、CHECK 约束等。

-   约束 (Constraints): MySQL 支持各种约束，如主键 (PRIMARY KEY)、外键 (FOREIGN KEY)、唯一键 (UNIQUE)、非空 (NOT NULL)、检查约束 (CHECK) 等。这些约束在数据写入时被强制执行，确保数据满足预定义的规则。
-   触发器 (Triggers): 触发器是与表关联的存储程序，在特定事件 (如 INSERT、UPDATE、DELETE) 发生时自动执行。触发器可以用于实现更复杂的业务规则和一致性检查。
-   应用程序逻辑: 虽然 MySQL 提供了约束和触发器，但最终的数据一致性也需要应用程序逻辑来保证。例如，业务逻辑需要确保事务操作符合业务规则，才能维持数据库的一致性状态。

其实对于一致性来说，**它是其他三者（原子性、隔离性、持久性）的综合结果，辅以数据库约束和应用校验来共同保障最终一致性。**

## **总结**

MySQL通过以下核心机制实现ACID：

| ACID特性 | 核心机制 | 关键组件 |
| --- | --- | --- |
| **原子性** | Undo Log + 事务状态管理 | Undo Log、事务控制块 |
| **一致性** | 约束 + ACID协同 | 主键、外键、触发器 |
| **隔离性** | MVCC + 锁 + Next-Key Locks | Read View、行锁、间隙锁 |
| **持久性** | Redo Log + Doublewrite Buffer | Redo Log、双写缓冲区 |

我们经常说的**最终一致性**是其他三个特性协同作用的结果，而非独立机制。当你理解了这些底层原理将会有助于优化事务设计（如合理选择隔离级别）和故障排查（如分析锁冲突）。

