适用版本:MySQL 8.0.x(本文示例基于 InnoDB 存储引擎)
一、背景
在数据库开发中,我们经常会遇到这样一类操作:一组 SQL 语句必须要么全部成功,要么全部失败。
例如转账场景:
- 从 A 账户扣减余额;
- 给 B 账户增加余额;
- 记录一条转账流水。
如果第一步成功了,第二步失败了,那么数据就会进入一种“不一致”的状态。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只回退到某个阶段;- 不会结束整个事务;
- 最后仍然需要
COMMIT或ROLLBACK。
六、示例:用事务保证转账正确性
先准备表:
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 UPDATESELECT ... 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 对关键操作保留审计或流水
事务保证的是数据库状态一致,但很多业务还需要可追踪。对支付、库存、账户类业务,最好记录操作流水,便于审计与排错。
十、小结
事务是数据库中最基础、也最重要的能力之一。理解事务,不能只停留在“BEGIN、COMMIT、ROLLBACK”这几个关键字上,更要理解它背后的 ACID 思想。
可以把本文内容浓缩成下面几句话:
- 原子性依赖 Undo Log,保证失败时可回滚;
- 一致性是事务追求的目标,需要数据库与业务规则共同保证;
- 隔离性依赖锁、MVCC 和隔离级别,解决并发冲突;
- 持久性依赖 Redo Log,保证提交结果在故障后仍可恢复。
如果把事务理解透了,后续再学习:
- 隔离级别;
- MVCC;
- 行锁与间隙锁;
- 死锁分析;
就会顺畅很多。
📝 版权声明:本文为原创技术博客,转载请注明出处。
如文章中存在错误或不准确之处,欢迎在评论区指正,感谢您的阅读与支持!