事务:要么全做,要么全不做
📖 本节导读
- 通过银行转账的经典例子,理解为什么需要"事务"这个东西
- 认识事务的四大特性 ACID(用大白话解释,不背概念)
- 学会 MySQL 中事务的三个核心命令:
START TRANSACTION、COMMIT、ROLLBACK - 了解自动提交(autocommit)机制,以及隔离级别的基本常识(了解即可)
一、从一次银行转账说起
想象一个场景:张三要给李四转账 200 元。在数据库里,这件事其实要分两步完成:
- 第一步:从张三的账户里扣掉 200 元
- 第二步:往李四的账户里加上 200 元
用 SQL 写出来大概是这样:
UPDATE bank_account SET balance = balance - 200 WHERE name = '张三';
UPDATE bank_account SET balance = balance + 200 WHERE name = '李四';
看起来没问题?现在想一个可怕的情况:
第一步执行成功了,张三的钱扣掉了。就在这时,数据库服务器突然断电,第二步没来得及执行。
结果是什么?张三少了 200 元,李四却一分钱没收到,200 元凭空消失了!这在银行系统里是绝对不能接受的。
我们真正想要的效果是:
这两步操作要么全部成功,要么全部失败。绝不允许"做了一半"。
这就是**事务(Transaction)**要解决的问题。
二、什么是事务
事务就是把一组 SQL 操作"打包"成一个不可分割的整体:
- 如果包里的每一步都成功了,就一起生效(提交)
- 只要其中任何一步出了问题,就全部撤销,就像什么都没发生过一样(回滚)
类比一下:事务就像网购的"下单"按钮——点一次要同时完成"扣库存、扣款、生成订单"三件事。不能出现"钱扣了但订单没生成"的情况,要么整单成功,要么整单取消退款。
三、事务的四大特性:ACID
事务有四个著名的特性,合称 ACID(四个英文单词的首字母)。用大白话解释:
| 特性 | 英文 | 通俗解释 |
|---|---|---|
| 原子性 | Atomicity | 事务是一个"原子",不可再分。要么全做,要么全不做,没有中间状态 |
| 一致性 | Consistency | 事务执行前后,数据总量是"对得上账"的。转账前后,张三 + 李四的总钱数不变 |
| 隔离性 | Isolation | 多个人同时操作数据库时,各自的事务互不干扰,就像每个人在独立的房间里办业务 |
| 持久性 | Durability | 一旦提交成功,数据就永久保存了,哪怕下一秒断电,重启后数据还在 |
新手阶段记住一句话就够了:事务保证一组操作"要么全成,要么全不成",而且提交后不会丢。
四、MySQL 中的事务操作
MySQL 中控制事务的核心命令只有三个:
| 命令 | 作用 |
|---|---|
START TRANSACTION;(或 BEGIN;) | 开启一个事务,从这里开始"打包" |
COMMIT; | 提交事务,让包里的所有修改真正生效 |
ROLLBACK; | 回滚事务,撤销包里的所有修改,回到开启事务之前的样子 |
START TRANSACTION和BEGIN效果完全一样,BEGIN更短,敲起来省事。
4.1 准备演示环境
我们在 school 库里临时建一张银行账户表来做实验(做完实验可以删掉,不影响其他章节):
USE school;
CREATE TABLE bank_account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
balance DECIMAL(10,2) NOT NULL
);
INSERT INTO bank_account (name, balance) VALUES
('张三', 1000.00),
('李四', 1000.00);
看一下初始状态,两人各有 1000 元:
SELECT * FROM bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
4.2 完整演示:ROLLBACK 撤销一切
下面模拟"转账做到一半反悔了"的场景。请完整跟着敲一遍:
-- 第 1 步:开启事务
START TRANSACTION;
-- 第 2 步:张三扣 200
UPDATE bank_account SET balance = balance - 200 WHERE name = '张三';
-- 第 3 步:李四加 200
UPDATE bank_account SET balance = balance + 200 WHERE name = '李四';
-- 第 4 步:先查一眼(注意:此时在"当前窗口"里能看到变化)
SELECT * FROM bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1200.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
咦,数据好像已经变了?别急——这只是"草稿",还没有真正生效。现在我们反悔,执行回滚:
-- 第 5 步:回滚!撤销上面两条 UPDATE
ROLLBACK;
-- 第 6 步:再查一次
SELECT * FROM bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
数据完好如初! 两条 UPDATE 就像从来没执行过一样。这就是事务的威力:在 COMMIT 之前,一切修改都只是草稿,随时可以用 ROLLBACK 一键撤销。
4.3 演示:COMMIT 让修改真正生效
这次我们把转账"办到底":
START TRANSACTION;
UPDATE bank_account SET balance = balance - 200 WHERE name = '张三';
UPDATE bank_account SET balance = balance + 200 WHERE name = '李四';
-- 确认没问题,提交!
COMMIT;
SELECT * FROM bank_account;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 800.00 |
| 2 | 李四 | 1200.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
COMMIT 之后修改就永久生效了,此时再执行 ROLLBACK 也无法撤销——回滚只对"还没提交的事务"有效。
实验做完,把临时表删掉,保持 school 库干净:
DROP TABLE bank_account;
五、autocommit:其实你一直在用事务
你可能会问:前面学 INSERT、UPDATE 的时候,从来没写过 COMMIT,为什么数据也保存了?
因为 MySQL 默认开启了自动提交(autocommit):
你执行的每一条 SQL 语句,MySQL 都自动把它当成一个独立的小事务,执行完立刻自动 COMMIT。
可以查看这个开关的状态:
SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
ON 表示开启。所以平时一条一条执行 SQL 时不用管事务;只有当你需要把多条语句捆绑成一个整体时,才手动 START TRANSACTION。一旦手动开启事务,自动提交就暂时"让位",直到你 COMMIT 或 ROLLBACK 为止。
六、隔离级别:了解即可,不必深究
当多个人同时操作同一批数据时,事务之间可能互相"打扰",典型的三种问题:
- 脏读:读到了别人还没提交的草稿数据。比如你查到张三余额 800,结果对方一回滚,这个 800 根本不存在。
- 不可重复读:同一个事务里,前后两次查同一行,结果不一样(因为中间被别人改了并提交了)。比如第一次查余额 1000,第二次查变成 800。
- 幻读:同一个事务里,前后两次查同一个范围,多出了新的行(别人插入了新数据)。比如第一次数有 5 个学生,第二次数变成 6 个,像出现了"幻影"。
为了控制这些问题,SQL 标准定义了四个隔离级别,从松到严:
READ UNCOMMITTED(读未提交)—— 最宽松,三种问题都可能发生READ COMMITTED(读已提交)—— 解决脏读REPEATABLE READ(可重复读)—— 再解决不可重复读,MySQL 的默认级别SERIALIZABLE(串行化)—— 最严格,事务排队执行,性能最差
查看当前隔离级别:
SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
💡 给新手的话:隔离级别属于进阶知识,涉及并发场景,日常学习和小项目用 MySQL 默认的
REPEATABLE READ就完全够用。现在知道有这回事、看得懂这几个名词即可,不必深究,更不用急着去修改它。
⚠️ 新手常见坑
- 以为事务里 SELECT 查到变化 = 已经保存了。在
COMMIT之前,你在当前窗口看到的修改只是"草稿",其他窗口的用户看不到;断开连接或回滚,草稿就没了。 - COMMIT 之后想 ROLLBACK。提交是"泼出去的水",回滚只能撤销未提交的修改。所以提交前请再三确认。
- 执行 DDL 语句会隐式提交。在事务里执行
CREATE TABLE、DROP TABLE、ALTER TABLE等语句,MySQL 会自动把之前的操作先提交掉,之后就回滚不了了。事务里尽量只放增删改查。 - 开了事务忘记提交。手动
START TRANSACTION后一直不COMMIT,你的修改别人永远看不到,还可能长时间锁住数据,导致别人的操作卡住。事务要"快开快关"。 - 误以为只有 InnoDB 才支持事务是废话。这一点确实要知道:MySQL 默认的 InnoDB 存储引擎支持事务,而老的 MyISAM 引擎不支持。现在默认建表都是 InnoDB,一般不用操心。
📝 小结
- 事务把多条 SQL 打包成整体:要么全成功(COMMIT),要么全撤销(ROLLBACK)
- ACID:原子性、一致性、隔离性、持久性
- 核心三命令:
START TRANSACTION(或BEGIN)→ 执行 SQL →COMMIT或ROLLBACK - MySQL 默认
autocommit = ON:不手动开事务时,每条语句自动独立提交 - 隔离级别有四档,MySQL 默认
REPEATABLE READ,新手了解即可
✍️ 练习题
1. 判断题:执行了 COMMIT 之后,还可以用 ROLLBACK 撤销刚才的修改。
参考答案
错误。ROLLBACK 只能撤销尚未提交的事务。一旦 COMMIT,修改就永久生效(持久性),无法回滚。
2. 张三要给李四转 500 元,请写出用事务完成转账的完整 SQL(假设 bank_account 表存在)。
参考答案
START TRANSACTION;
UPDATE bank_account SET balance = balance - 500 WHERE name = '张三';
UPDATE bank_account SET balance = balance + 500 WHERE name = '李四';
COMMIT;
如果中途发现问题(比如余额不足),用 ROLLBACK; 代替 COMMIT; 撤销全部操作。
3. ACID 中的"原子性"是什么意思?请用自己的话解释。
参考答案
原子性指事务是一个不可分割的最小单位:事务里的所有操作要么全部执行成功,要么全部不执行,不允许出现"只做了一半"的中间状态。比如转账的扣款和入账必须同生共死。
4. 小明在事务中执行了一条 UPDATE,然后在同一个窗口 SELECT 看到数据变了,于是直接关掉了窗口。请问这条 UPDATE 最终生效了吗?为什么?
参考答案
没有生效。手动开启的事务在未 COMMIT 时,修改只是"草稿"。直接断开连接,MySQL 会自动回滚未提交的事务,数据恢复原样。
5. MySQL 默认的事务隔离级别是哪个?"脏读"是什么意思?
参考答案
默认隔离级别是 REPEATABLE READ(可重复读)。脏读是指一个事务读到了另一个事务尚未提交的数据——如果对方随后回滚了,你读到的就是一份"根本不存在"的脏数据。默认的 REPEATABLE READ 级别不会发生脏读。