人人都会AI编程

6.1 事务四大特性(ACID)底层保障

更新时间:2026-07-11

事务的 ACID 常被挂在嘴边,但真正理解数据库是如何在底层实现这些特性的,才能帮助你在遇到线上问题时快速定位“是哪个环节出了问题”。在 MySQL 的 InnoDB 引擎中,ACID 并不是四个孤立的口号,而是分别由 Undo Log、Redo Log、锁机制和一系列完整性机制协同保障的。

原子性:要么全做,要么全不做

原子性意味着一个事务中的所有操作,在逻辑上不可分割。成功则全部生效,失败则全部撤销,不允许只运行一部分。InnoDB 通过 Undo Log(回滚日志) 来实现这一点。

每当你执行一条 INSERT、UPDATE 或 DELETE 时,InnoDB 在修改数据页之前,会先把“如何改回去”的信息记录到 Undo Log 中。例如:

  • 插入一条记录时,Undo Log 记录这条记录的主键值,回滚时直接删掉它。
  • 更新一个字段时,Undo Log 记录该字段的旧值,回滚时用旧值覆盖回去。
  • 删除一条记录时,Undo Log 记录整行数据的完整内容,回滚时重新插入。

如果事务执行到一半主动执行 ROLLBACK,或者 MySQL 在事务未提交时崩溃,重启后 InnoDB 会扫描 Undo Log,找到所有未提交的事务,按 Undo 里记录的反向操作逐个回滚,最终数据库内部只会保留已提交事务的结果,不会存在“做了一半”的中间状态。

一个容易被忽略的细节:Undo Log 本身也是以页的形式存储在表空间中的,它的修改同样会产生 Redo Log,以保证 Undo 本身也不会在崩溃时丢失。这种“日志的日志”设计,确保了回滚操作的可靠性。

实际开发中,理解原子性的意义在于:只要你把相关操作包裹在 BEGIN/COMMIT/ROLLBACK 中,就无需担心“三条 SQL 成功了两条”的情况。错误处理变得非常干净。

一致性:数据从头到尾满足所有规则

一致性是整个事务的终极目标:事务开始前数据是合法的,事务结束后数据仍然是合法的。这里的“合法”并不仅仅指没有脏数据,而是涵盖多个层面。

在 InnoDB 中,一致性不是由某个单一技术点来保障的,而是由以下机制共同完成:

  • 约束检查:主键唯一、外键引用、NOT NULL 等约束由存储引擎在修改数据时强制校验。如果一条 INSERT 违反了唯一约束,语句立刻失败,事务要么回滚整条语句,要么完全回滚(取决于业务逻辑)。
  • 数据类型与格式检查:在严格模式(STRICT_TRANS_TABLES)下,插入的数值若超出范围或格式错误,会直接报错而非默默截断。
  • Double Write 机制:为了防止数据页部分写失效(比如写了一半时断电,页被损坏),InnoDB 在刷脏页到磁盘前,会先将页的内容写入双写缓冲区(Double Write Buffer)的连续空间,再分两次写入表中。崩溃恢复时,如果发现数据页损坏,可以用双写缓冲区里的完整副本修复。这在底层保证了“数据页本身”的物理一致性。
  • 崩溃恢复机制:利用 Redo Log 和 Undo Log 协同工作,确保崩溃后能将数据库恢复到“所有已提交事务生效、所有未提交事务作废”的一致状态。
  • 事务隔离性:并发控制(MVCC 与锁)保证事务不会读取到其他事务的中间状态,从而在逻辑上维持数据一致。

一个常见的误区是认为“一致性是靠应用程序代码保证的”。实际上,数据库层面的约束和日志机制是最底层的防护墙,应用程序更像是在这之上构建业务规则。如果只是把检查逻辑全丢给代码,而关掉数据库约束,一旦出现绕过应用的直接操作或代码 Bug,数据就可能被破坏。

隔离性:多个事务同时跑,彼此看不见对方的中间状态

当多个事务同时读写同一条数据时,如果不加以控制,就会产生脏读、不可重复读、幻读等问题。InnoDB 通过MVCC两套机制,实现了不同隔离级别的并发控制。

锁机制负责处理“写写冲突”。事务在修改某行时会加排他锁(X 锁),其他事务若同时要修改同一行,必须等待。而行级锁的精确粒度,使得事务修改不同行时可以完全并行。

MVCC(多版本并发控制)则专门优化“读写并发”。它避免读写互相阻塞:当一个事务在写某行时,其他事务依然可以读取该行的历史版本,而不需要等待锁释放。MVCC 的实现依赖:

  • 每行数据有隐藏列记录最近修改该行的事务 ID(DB_TRX_ID)和指向 Undo Log 中旧版本的回滚指针(DB_ROLL_PTR)。
  • 事务启动时生成一个 Read View,记录当前活跃的事务快照。
  • 读取一条记录时,InnoDB 会根据 Read View 沿着 Undo Log 的版本链回溯,找到一个对当前事务可见的版本返回。

不同隔离级别下,Read View 的生成时机不同,使得可见性规则有所差异,从而产生不同的事务现象:

  • 读未提交:不生成 Read View,直接读最新版本(包括未提交的),会出现脏读。
  • 读已提交:每次 SQL 执行时生成新的 Read View,可见提交的事务,避免脏读,但可能出现不可重复读。
  • 可重复读:事务开始时生成一个 Read View,整个事务都共用这个快照,避免不可重复读,加上 InnoDB 特有的间隙锁,能在很大程度上避免幻读。
  • 串行化:所有读操作自动加共享锁,读写串行执行,完全避免并发问题,但性能极低。

对开发者而言,关键是理解:InnoDB 默认的可重复读级别已经能屏蔽绝大多数并发怪异现象,而一旦你调整了隔离级别,就要清楚由此带来的数据可见性变化和潜在的性能代价。

持久性:一旦提交,数据绝不丢失

事务的持久性意味着,只要客户端收到 COMMIT 成功的响应,即使数据库在最糟糕的时刻(例如提交后一毫秒宕机)崩溃,这个事务的结果也不会丢失。InnoDB 依靠 Redo Log(重做日志)Binlog 的两阶段提交来保证持久性。

Redo Log 的工作原理

Redo Log 是物理日志,记录“对哪个表空间的数据页做了什么修改”。它存储的是修改后的值,并且采用循环写入的方式。当事务提交时,InnoDB 需要保证 Redo Log 已经被写入磁盘,这由 innodb_flush_log_at_trx_commit 参数控制:

  • 设为 1(最安全):每次提交都将 Redo Log 缓冲区的数据写入日志文件系统,并立即刷新到磁盘。即使宕机,已提交的事务也能通过 Redo Log 重放恢复。
  • 设为 02:性能更高,但崩溃时可能丢失最近一秒内提交的事务。生产环境如果对数据零丢失要求极高,务必设为 1

两阶段提交

除了 InnoDB 自身的 Redo Log,MySQL 服务层还会写入 Binlog(逻辑日志,记录 SQL 语句或行的变化)。为保证 Redo Log 和 Binlog 在崩溃后保持一致(不会出现主库恢复后 Binlog 缺失导致从库数据不一致),MySQL 采用了两阶段提交:

  1. Prepare 阶段:InnoDB 将事务的 Redo Log 写入并刷盘,标记事务为 prepare 状态。
  2. Commit 阶段:服务层写入 Binlog 并刷盘,之后 InnoDB 将 Redo Log 中对应的事务标记为 commit 状态。

如果崩溃发生在 prepare 之后、写入 Binlog 之前,事务会被回滚;如果发生在写入 Binlog 之后,事务会被提交。无论哪种情况,都能保证 Redo Log 和 Binlog 中记录的事务集合完全一致。

从开发者角度理解持久性

你不需要去操心 Redo Log 循环写的具体细节,但只要记住两件事:

  • COMMIT 成功返回 = 数据已经安全落地。不需要自己再做额外持久化。
  • 为了这份安全感,每次事务提交都有一次磁盘顺序写入的成本。如果大量小事务频繁提交,Redo Log 写入可能成为瓶颈,此时可以考虑将多个操作合并到一个事务中批量提交,减少刷盘次数。

综上,事务的 ACID 并不是魔法,而是由一系列精细设计的日志、锁和校验机制堆叠出来的工程实现。理解这些底层的“安全网”如何布设,你就能更客观地评估系统的可靠性边界,也知道在遇到性能问题时可以调整哪些参数、不能轻易牺牲哪一层保障。