在前一篇文章中,我们学习了索引如何提升查询性能。但在真实业务场景中,一个操作往往包含多个 SQL 语句,这些语句要么全部成功,要么全部失败,不能出现"只执行了一半"的情况。比如银行转账:A 账户扣款和 B 账户加款必须同时生效,否则资金就凭空消失或凭空产生了。这就是数据库事务要解决的问题。
本文将带你系统地学习 MySQL 事务的核心知识,内容包括:
- 什么是事务,为什么需要事务
- 事务的四大特性:ACID
- 显式事务操作:
START TRANSACTION、COMMIT、ROLLBACK - 自动提交模式
autocommit的作用 - 通过银行转账案例动手体验事务
- 并发访问下的问题初探:脏读、不可重复读、幻读
读完本文,你将能正确地在应用中使用事务来保证数据的一致性,并为后续深入理解隔离级别和锁机制打下基础。
1. 什么是事务?
事务(Transaction) 是数据库管理系统执行过程中的一个逻辑单位,它由一组有限的数据库操作序列构成。事务具有"要么全部执行,要么全部不执行"的原子特性。
用转账的例子来说:从账户 A 转 100 元到账户 B,包含两步:
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
如果第一步执行成功后系统突然崩溃,第二步没有执行,那么 A 的钱被扣除了,B 却没收到。这 100 元就凭空消失了。如果将这两个操作放在一个事务中,数据库就能保证要么两步都执行成功,要么两步都不执行(回滚到执行前的状态)。
2. 事务的四大特性:ACID
ACID 是衡量事务正确性的四个基本要素:
- 原子性 (Atomicity):事务中的所有操作作为一个整体,要么全部完成,要么全部不做。由 Undo Log 实现,后面原理篇会详解。
- 一致性 (Consistency):事务执行前后,数据库必须从一个一致性状态转换到另一个一致性状态。例如转账前后总金额不变。一致性由原子性、隔离性和持久性共同保证,也需要业务逻辑本身正确。
- 隔离性 (Isolation):多个事务并发执行时,彼此之间不能互相干扰。隔离级别越高,并发度越低。MySQL InnoDB 通过锁和 MVCC 实现隔离性。
- 持久性 (Durability):一旦事务提交,其对数据库的修改就是永久的,即使系统崩溃也不会丢失。由 Redo Log 保证。
我们可以用一个简单的比喻来记忆:A (原子)是事务的"不可分割",C (一致)是"规则不被打破",I (隔离)是"互不干扰",D(持久)是"说到做到"。
3. 事务的基本操作:开启、提交、回滚
MySQL 中,InnoDB 存储引擎支持事务。默认情况下,MySQL 为每条单独的 SQL 语句自动开启一个事务并自动提交(autocommit 模式)。如果需要显式控制事务边界,可以手动开启事务。
3.1 显式事务语法
sql
-- 开启事务
START TRANSACTION;
-- 或者使用 BEGIN; (START TRANSACTION 是标准 SQL)
-- 执行一系列 SQL 操作
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
-- 如果一切正常,提交事务,使修改永久生效
COMMIT;
-- 如果中间出现问题,回滚事务,撤销所有修改
ROLLBACK;
3.2 autocommit 模式
通过以下命令查看当前会话的自动提交设置:
sql
SHOW VARIABLES LIKE 'autocommit';
autocommit = 1(默认):每条 SQL 语句自动成为一个事务,执行后立即提交。autocommit = 0:需要显式使用COMMIT或ROLLBACK来结束事务,否则连接断开时自动回滚。
你可以临时关闭自动提交来体验事务:
sql
SET autocommit = 0;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
-- 此时如果发现误操作,可以回滚
ROLLBACK;
SET autocommit = 1; -- 恢复默认
建议在应用程序中始终使用 START TRANSACTION 明确开启事务,而不要依赖 autocommit = 0,因为后者容易遗漏提交或回滚,造成长事务锁表。
4. 实战:银行转账案例
我们来创建一个账户表并模拟一次转账,亲手感受事务的原子性和一致性。
4.1 准备账户表
sql
CREATE TABLE accounts (
id INT PRIMARY KEY,
owner VARCHAR(50) NOT NULL,
balance DECIMAL(10, 2) NOT NULL DEFAULT 0.00
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO accounts VALUES
('A', '张三', 500.00),
('B', '李四', 300.00);
-- 查看初始余额
SELECT * FROM accounts;
4.2 成功转账(提交事务)
sql
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
-- 此时新开一个会话查询,看不到未提交的修改(隔离性)
-- 在当前会话内可以查看结果:
SELECT * FROM accounts;
COMMIT; -- 提交后,其他会话才能看到修改
4.3 模拟失败(回滚事务)
假设 A 账户余额不足,我们检测到并决定回滚:
sql
START TRANSACTION;
UPDATE accounts SET balance = balance - 600 WHERE id = 'A'; -- A 只有500
-- 在应用中检查 balance 是否为负数,如果是:
ROLLBACK;
-- 查看余额,应该和转账前一样
SELECT * FROM accounts;
这样,即使误操作导致负数,回滚也能保证数据恢复到初始状态。
4.4 清理环境
sql
DROP TABLE IF EXISTS accounts;
5. 初识并发问题:脏读、不可重复读、幻读
当多个事务同时操作同一批数据时,如果不采取适当的隔离措施,就可能出现以下三种典型的并发问题。这里先用直觉理解,后续我们会详细讲解 InnoDB 的隔离级别和锁机制。
5.1 脏读 (Dirty Read)
一个事务读取到了另一个事务尚未提交的修改数据。如果那个事务随后回滚了,读到的数据就是"脏"的,不再有效。
举例:
- 事务 T1 将 A 的余额改为 400,但未提交。
- 事务 T2 此时读取 A 的余额,得到 400。
- T1 因某种原因回滚,余额恢复为 500。
- T2 读到的 400 就是一个"脏"数据,基于它做出的业务决策可能出错。
5.2 不可重复读 (Non-Repeatable Read)
一个事务内,两次读取同一行数据,得到的值不同(因为被其他已提交事务修改了)。
举例:
- 事务 T1 读取 A 的余额,得到 500。
- 事务 T2 将 A 的余额改为 400 并提交。
- 事务 T1 再次读取 A 的余额,得到 400。
- T1 在同一个事务中两次读到的值不一致。
不可重复读关注的是同一行数据的值被修改。
5.3 幻读 (Phantom Read)
一个事务内,两次执行相同的范围查询,结果集的行数不同(因为其他事务插入了满足条件的新行)。
举例:
- 事务 T1 查询余额大于 300 的账户,返回 {张三, 李四}。
- 事务 T2 插入了一条余额为 400 的新账户并提交。
- 事务 T1 再次查询余额大于 300 的账户,返回 {张三, 李四, 王五}。
- T1 发现结果集的行变多了,就像出现了"幻影"。
不可重复读针对的是行内容的修改 ,幻读针对的是行数量的变化(插入或删除)。
6. 事务隔离级别简介
为了解决上述并发问题,SQL 标准定义了四种事务隔离级别,由低到高依次为:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ (InnoDB 默认) | 不可能 | 不可能 | 可能(InnoDB 通过 Next-Key Lock 防止大部分幻读) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
MySQL InnoDB 的默认隔离级别是 REPEATABLE READ ,它通过 MVCC(多版本并发控制) 解决了不可重复读,并通过 临键锁(Next-Key Lock) 在很大程度上抑制了幻读。我们将在第五阶段深入这些机制的实现原理,目前只需理解每种隔离级别能解决哪些问题即可。
查看和设置隔离级别:
sql
-- 查看当前会话的隔离级别
SELECT @@transaction_isolation;
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 设置当前会话的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
7. 实战:观察脏读现象
我们通过两个并发的会话,体验 READ UNCOMMITTED 级别下的脏读。
准备工作:创建测试表并插入数据。
sql
CREATE TABLE dirty_test (
id INT PRIMARY KEY,
value INT
) ENGINE=InnoDB;
INSERT INTO dirty_test VALUES (1, 100);
会话 A(操作方):
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE dirty_test SET value = 200 WHERE id = 1;
-- 注意:尚未提交!
会话 B(观察方):
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT * FROM dirty_test WHERE id = 1; -- 结果:value = 200 (脏读!)
此时会话 B 读到了未提交的 200。如果会话 A 执行 ROLLBACK,数据恢复 100,但 B 基于 200 的后续操作就出错了。
会话 A 回滚:
sql
ROLLBACK;
会话 B 再次查询:
sql
SELECT * FROM dirty_test WHERE id = 1; -- 结果:value = 100
清理:
sql
DROP TABLE dirty_test;
这个实验直观展示了脏读的危害。在实际业务中,我们绝不希望读到未提交的数据,因此生产环境至少使用 READ COMMITTED 或更高隔离级别。
8. 小结
事务是保证数据库操作正确性的基石:
- 事务具有 ACID 特性:原子性、一致性、隔离性、持久性。
- 我们可以用
START TRANSACTION/COMMIT/ROLLBACK手动控制事务边界。 autocommit控制每条 SQL 是否自动提交,建议保持默认1,显式开启事务。- 通过银行转账案例,你亲手验证了原子性和一致性;通过脏读实验,你看到了隔离性缺失带来的风险。
- 并发访问会引发脏读、不可重复读、幻读三类问题,隔离级别越高越安全,但并发性能越低。InnoDB 默认
REPEATABLE READ在多数场景下是性能和安全的较好平衡。
事务的底层实现(Undo Log、Redo Log、MVCC、锁)会在第四和第五阶段深度剖析。下一篇我们将进入 JDBC 编程,学习如何用 Java 连接 MySQL 并在代码中控制事务,迈出数据库与应用程序集成的第一步。
思考题:
- 在 autocommit=1 的情况下,执行一条
UPDATE时,MySQL 内部发生了什么?(提示:隐式开启事务、执行、自动提交) - 为什么"不可重复读"和"幻读"有区别?举一个幻读但非不可重复读的例子。
- 试着设置隔离级别为
SERIALIZABLE并运行转账案例,观察并发时的行为差异(可以模拟锁等待)。
参考资料
- MySQL 8.0 Reference Manual - Transactions
- MySQL 8.0 Reference Manual - START TRANSACTION, COMMIT, and ROLLBACK
- MySQL 8.0 Reference Manual - Transaction Isolation Levels