返回首页

14 |事务与 ACID 特性

适用版本:MySQL 8.0.x(本文示例基于 InnoDB 存储引擎)

一、背景

在数据库开发中,我们经常会遇到这样一类操作:一组 SQL 语句必须要么全部成功,要么全部失败。

例如转账场景:

  1. 从 A 账户扣减余额;
  2. 给 B 账户增加余额;
  3. 记录一条转账流水。

如果第一步成功了,第二步失败了,那么数据就会进入一种“不一致”的状态。A 的钱少了,但 B 没收到钱,这显然是业务无法接受的。

为了解决这类问题,数据库引入了事务(Transaction)。事务的核心目标是:把一组逻辑相关的操作打包成一个不可分割的整体,由数据库保证其执行结果的正确性与一致性。

在 MySQL 里,真正完整支持事务的是 InnoDB 引擎,因此讨论事务时,默认通常也是在讨论 InnoDB 事务。


二、什么是事务

事务可以理解为一组 SQL 操作组成的工作单元,这个单元具备如下特征:

  • 可以显式开始;
  • 可以在中途执行多条语句;
  • 最后通过 COMMIT 提交;
  • 如果出现异常,可以通过 ROLLBACK 回滚。

一个最简单的事务示例:

START TRANSACTION;

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

COMMIT;

如果中途发现问题:

START TRANSACTION;

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

ROLLBACK;

回滚后,前面的修改不会保留。


三、ACID 是什么

事务最经典的四个特性就是 ACID

  • A:Atomicity(原子性)
  • C:Consistency(一致性)
  • I:Isolation(隔离性)
  • D:Durability(持久性)

很多初学者会背这四个词,但真正落到数据库实现层面时,会发现 ACID 不是一句口号,而是一整套机制共同作用的结果。


四、核心机制:ACID 四大特性的含义与实现思路

4.1 原子性:要么都做,要么都不做

原子性强调的是:事务中的多个操作不能只执行一半。

比如转账事务里包含两条更新语句,如果第一条执行成功,第二条失败,那么数据库必须能撤销第一条更新带来的影响。

InnoDB 实现原子性的重要基础是:

  • Undo Log(回滚日志)

Undo Log 会记录数据修改前的旧版本信息。当事务需要回滚时,InnoDB 可以根据 Undo Log 将数据恢复到修改之前的状态。

可以把 Undo Log 理解成“后悔药记录本”。只要事务还没真正结束,数据库就保留回退路径。

示例:原子性演示

START TRANSACTION;

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

-- 假设发现业务校验失败
ROLLBACK;

回滚后,A 和 B 的余额都会恢复到事务开始前的状态。


4.2 一致性:事务前后,数据要满足约束

一致性是最容易被误解的概念。它不是单纯指“数据看起来没问题”,而是指:

  • 事务执行前,数据库满足业务约束;
  • 事务执行后,数据库仍然满足业务约束。

例如:

  • 账户余额不能为负数;
  • 订单金额必须等于明细金额之和;
  • 外键关系不能被破坏。

一致性不是只靠数据库自动完成的,而是由多方面共同保证:

  • 应用逻辑正确;
  • 约束定义合理;
  • 事务具备原子性、隔离性、持久性。

也就是说,一致性更像目标,ACID 中其他三个特性更像实现一致性的手段之一

示例:一致性演示

CREATE TABLE account (
    id BIGINT PRIMARY KEY,
    balance DECIMAL(10,2) NOT NULL CHECK (balance >= 0)
);

如果事务执行后试图让 balance 变成负数,那么这类约束应被数据库或应用逻辑阻止,否则一致性就被破坏了。


4.3 隔离性:并发事务之间互不干扰

数据库不可能永远只有一个事务在运行。真实系统里,常常有几十、几百甚至几千个事务同时执行。

如果事务之间互相读取到对方尚未提交的数据,或者在执行过程中被其他事务反复影响,就可能产生各种并发问题,例如:

  • 脏读;
  • 不可重复读;
  • 幻读。

隔离性强调的是:多个并发事务在逻辑上尽量“像串行执行一样正确”。

MySQL 通过以下机制共同实现隔离性:

  • 锁(Lock):限制并发读写冲突;
  • MVCC(多版本并发控制):提供一致性读,降低读写冲突;
  • 隔离级别:定义事务之间可见性的严格程度。

隔离越强,数据越稳定;但通常并发性能也会受到更多影响。


4.4 持久性:提交后的结果不能丢

持久性要求事务一旦提交,即使数据库进程崩溃、机器断电,已经提交的数据也必须能够恢复出来。

InnoDB 实现持久性的关键依赖:

  • Redo Log(重做日志)

事务提交时,数据修改会先记录到 Redo Log 中。即使内存中的脏页还没来得及刷回磁盘,系统宕机后也可以通过 Redo Log 重放已提交的修改。

这就是常说的 WAL(Write-Ahead Logging,预写日志) 思想:

先写日志,再写数据页。

示例:持久性理解

START TRANSACTION;
UPDATE account SET balance = balance + 500 WHERE id = 3;
COMMIT;

只要 COMMIT 成功返回,数据库就有义务在故障恢复后仍然保留这次更新结果。


五、事务控制语句的基本用法

在 MySQL 中,常见的事务控制语句有:

5.1 开启事务

START TRANSACTION;

或:

BEGIN;

5.2 提交事务

COMMIT;

5.3 回滚事务

ROLLBACK;

5.4 设置保存点

保存点适合在长事务中做阶段性回退。

START TRANSACTION;

UPDATE product SET stock = stock - 1 WHERE id = 1001;
SAVEPOINT sp_order;

INSERT INTO orders(id, user_id, amount) VALUES (1, 10, 199);

-- 如果插入订单失败,可以回滚到保存点
ROLLBACK TO sp_order;

COMMIT;

注意:

  • ROLLBACK TO SAVEPOINT 只回退到某个阶段;
  • 不会结束整个事务;
  • 最后仍然需要 COMMITROLLBACK

六、示例:用事务保证转账正确性

先准备表:

CREATE TABLE account (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    balance DECIMAL(10,2) NOT NULL
);

INSERT INTO account VALUES
(1, 'Alice', 1000.00),
(2, 'Bob', 800.00);

执行事务:

START TRANSACTION;

UPDATE account
SET balance = balance - 200
WHERE id = 1;

UPDATE account
SET balance = balance + 200
WHERE id = 2;

COMMIT;

如果想加上余额校验,实际项目中通常会这样写:

START TRANSACTION;

UPDATE account
SET balance = balance - 200
WHERE id = 1
  AND balance >= 200;

-- 检查影响行数,若为 0 说明余额不足
UPDATE account
SET balance = balance + 200
WHERE id = 2;

COMMIT;

如果第一条更新未成功,就应回滚整个事务,而不是让第二条继续提交。


七、自动提交机制要特别注意

MySQL 默认开启 autocommit=1,即每条单独执行的 SQL 默认都是一个独立事务。

例如:

SHOW VARIABLES LIKE 'autocommit';

如果自动提交开启,那么下面两条语句并不会天然属于同一个事务:

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

它们会分别提交,这就失去了“整体成功或整体失败”的事务保障。

如果要把多条语句放进同一个事务,必须:

  • 显式执行 START TRANSACTION;或
  • 临时关闭自动提交。
SET autocommit = 0;

不过在实际应用里,更推荐显式事务边界,而不是长期关闭自动提交,避免连接池中的连接状态混乱。


八、常见问题

8.1 事务是不是只要用了 BEGIN 就一定生效?

不一定。

前提是表必须支持事务,例如 InnoDB。若使用 MyISAM 之类不支持事务的引擎,ROLLBACK 实际上无法撤销已执行的数据修改。


8.2 SELECT 语句也在事务里吗?

可以在事务里。

普通 SELECT 在 InnoDB 中通常是一致性非锁定读,借助 MVCC 读取某个快照版本;但如果使用:

  • SELECT ... FOR UPDATE
  • SELECT ... LOCK IN SHARE MODE(8.0 中更推荐 FOR SHARE

则会进入锁定读。


8.3 提交了事务,数据为什么还没立刻看到磁盘文件变化?

因为事务提交的“持久”并不意味着数据页立即完整刷盘。InnoDB 可以先依赖 Redo Log 保证崩溃恢复能力,数据页再异步刷回磁盘。


8.4 长事务为什么常被认为危险?

因为长事务会带来很多副作用:

  • 长时间占用锁;
  • 导致 Undo Log 无法及时清理;
  • 影响 purge;
  • 增加死锁与阻塞概率;
  • 拉高主从延迟与回滚成本。

所以事务不是越大越安全,而是要控制边界。


九、实践建议

9.1 事务要尽量短小

尽量只把真正需要原子性的 SQL 放入事务,不要把以下内容放进事务中间:

  • 远程接口调用;
  • 大量循环处理;
  • 人工等待;
  • 长时间计算。

9.2 先设计业务约束,再设计事务

事务能保证“整体性”,但不能替代业务规则。比如余额不能为负、库存不能超卖,这些条件仍要在表结构或 SQL 条件中体现出来。

9.3 明确成功与失败分支

写事务代码时要明确:

  • 哪一步失败需要整体回滚;
  • 哪些异常是可重试的;
  • 哪些错误应立即终止。

9.4 尽量使用 InnoDB

在 MySQL 8.0 中,InnoDB 已是默认主流选择。事务、行锁、MVCC、崩溃恢复等关键能力都依赖它。

9.5 对关键操作保留审计或流水

事务保证的是数据库状态一致,但很多业务还需要可追踪。对支付、库存、账户类业务,最好记录操作流水,便于审计与排错。


十、小结

事务是数据库中最基础、也最重要的能力之一。理解事务,不能只停留在“BEGINCOMMITROLLBACK”这几个关键字上,更要理解它背后的 ACID 思想。

可以把本文内容浓缩成下面几句话:

  • 原子性依赖 Undo Log,保证失败时可回滚;
  • 一致性是事务追求的目标,需要数据库与业务规则共同保证;
  • 隔离性依赖锁、MVCC 和隔离级别,解决并发冲突;
  • 持久性依赖 Redo Log,保证提交结果在故障后仍可恢复。

如果把事务理解透了,后续再学习:

  • 隔离级别;
  • MVCC;
  • 行锁与间隙锁;
  • 死锁分析;

就会顺畅很多。


📝 版权声明:本文为原创技术博客,转载请注明出处。

如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!

上一篇

13|查询优化实践

下一篇

15|隔离级别与并发问题