跳到主要内容

事务:要么全做,要么全不做

📖 本节导读

  • 通过银行转账的经典例子,理解为什么需要"事务"这个东西
  • 认识事务的四大特性 ACID(用大白话解释,不背概念)
  • 学会 MySQL 中事务的三个核心命令:START TRANSACTIONCOMMITROLLBACK
  • 了解自动提交(autocommit)机制,以及隔离级别的基本常识(了解即可)

一、从一次银行转账说起

想象一个场景:张三要给李四转账 200 元。在数据库里,这件事其实要分两步完成:

  1. 第一步:从张三的账户里扣掉 200 元
  2. 第二步:往李四的账户里加上 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 TRANSACTIONBEGIN 效果完全一样,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。一旦手动开启事务,自动提交就暂时"让位",直到你 COMMITROLLBACK 为止。

六、隔离级别:了解即可,不必深究

多个人同时操作同一批数据时,事务之间可能互相"打扰",典型的三种问题:

  • 脏读:读到了别人还没提交的草稿数据。比如你查到张三余额 800,结果对方一回滚,这个 800 根本不存在。
  • 不可重复读:同一个事务里,前后两次查同一行,结果不一样(因为中间被别人改了并提交了)。比如第一次查余额 1000,第二次查变成 800。
  • 幻读:同一个事务里,前后两次查同一个范围,多出了新的行(别人插入了新数据)。比如第一次数有 5 个学生,第二次数变成 6 个,像出现了"幻影"。

为了控制这些问题,SQL 标准定义了四个隔离级别,从松到严:

  1. READ UNCOMMITTED(读未提交)—— 最宽松,三种问题都可能发生
  2. READ COMMITTED(读已提交)—— 解决脏读
  3. REPEATABLE READ(可重复读)—— 再解决不可重复读,MySQL 的默认级别
  4. SERIALIZABLE(串行化)—— 最严格,事务排队执行,性能最差

查看当前隔离级别:

SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)

💡 给新手的话:隔离级别属于进阶知识,涉及并发场景,日常学习和小项目用 MySQL 默认的 REPEATABLE READ 就完全够用。现在知道有这回事、看得懂这几个名词即可,不必深究,更不用急着去修改它。

⚠️ 新手常见坑

  1. 以为事务里 SELECT 查到变化 = 已经保存了。在 COMMIT 之前,你在当前窗口看到的修改只是"草稿",其他窗口的用户看不到;断开连接或回滚,草稿就没了。
  2. COMMIT 之后想 ROLLBACK。提交是"泼出去的水",回滚只能撤销未提交的修改。所以提交前请再三确认。
  3. 执行 DDL 语句会隐式提交。在事务里执行 CREATE TABLEDROP TABLEALTER TABLE 等语句,MySQL 会自动把之前的操作先提交掉,之后就回滚不了了。事务里尽量只放增删改查。
  4. 开了事务忘记提交。手动 START TRANSACTION 后一直不 COMMIT,你的修改别人永远看不到,还可能长时间锁住数据,导致别人的操作卡住。事务要"快开快关"。
  5. 误以为只有 InnoDB 才支持事务是废话。这一点确实要知道:MySQL 默认的 InnoDB 存储引擎支持事务,而老的 MyISAM 引擎不支持。现在默认建表都是 InnoDB,一般不用操心。

📝 小结

  • 事务把多条 SQL 打包成整体:要么全成功(COMMIT),要么全撤销(ROLLBACK)
  • ACID:原子性、一致性、隔离性、持久性
  • 核心三命令:START TRANSACTION(或 BEGIN)→ 执行 SQL → COMMITROLLBACK
  • 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 级别不会发生脏读。


⬅ 上一节 | 🏠 返回目录 | 下一节 ➡