核心目标: 学习如何使用 MySQL 事务来保证一组数据库操作要么全部成功执行,要么全部失败回滚,从而维护数据的一致性和完整性。
事务概念简介
事务是一个或多个 SQL 语句组成的逻辑工作单元 (Unit of Work)。这个工作单元作为一个整体执行,要么所有语句都成功应用到数据库,要么所有语句的效果都被撤销。
ACID 属性 (事务的基本特性)
- A (Atomicity) 原子性: 事务不可分割,要么全做,要么全不做。
- C (Consistency) 一致性: 事务前后,数据库保持一致状态。
- I (Isolation) 隔离性: 并发事务互不干扰(由隔离级别控制)。
- D (Durability) 持久性: 事务一旦提交,更改即永久生效。
1. 事务控制语句
这些语句用于管理事务的开始、结束和状态。
- 开始事务 (
START TRANSACTION或BEGIN)
作用:显式地标志一个新事务的开始。后续 DML 语句纳入事务管理。
语法:
START TRANSACTION;
或者:
BEGIN;
可带选项:READ WRITE, READ ONLY, WITH CONSISTENT SNAPSHOT。
- 提交事务 (
COMMIT)
作用:将当前事务中进行的所有修改永久性地保存到数据库中。事务成功结束。
语法:
COMMIT;
- 回滚事务 (
ROLLBACK)
作用:撤销当前事务中进行的所有修改,使数据库恢复到事务开始之前的状态。事务失败结束。
语法:
ROLLBACK;
- 设置保存点 (
SAVEPOINT)
作用:在当前事务内部创建一个标记,允许后续回滚到这个点。
语法:
SAVEPOINT savepoint_name;
- 回滚到保存点 (
ROLLBACK TO SAVEPOINT)
作用:撤销从指定保存点之后执行的所有事务修改,事务并未结束。
语法:
ROLLBACK TO SAVEPOINT savepoint_name;
- 释放保存点 (
RELEASE SAVEPOINT)
作用:删除一个不再需要的保存点(通常非必需)。
语法:
RELEASE SAVEPOINT savepoint_name;
事务控制流程示例:
-- 假设账户表 accounts(account_id, balance)
-- 从账户 1 转账 100 到账户 2-- 1. 开始事务
START TRANSACTION;-- 2. 扣款
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;-- (可选) 设置保存点
SAVEPOINT point1;-- 3. 加款
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;-- 4. 判断并结束
-- 如果成功:
COMMIT;
-- 如果加款失败,想撤销加款但保留扣款(不常见,通常整个回滚):
-- ROLLBACK TO SAVEPOINT point1;
-- COMMIT; -- 提交保存点之前的操作
-- 如果整个操作需要撤销:
-- ROLLBACK;
2. 自动提交
作用:控制 MySQL 是否自动将每条单独的 SQL 语句视为一个事务并立即提交。
- 查看当前自动提交状态:
语法:
SELECT @@autocommit; -- 1=启用(默认), 0=禁用
- 禁用自动提交:
需要显式使用START TRANSACTION,COMMIT,ROLLBACK。
语法:
SET autocommit = 0; -- 对当前会话有效
- 启用自动提交:
每条 DML 语句自动提交(除非在显式事务块内)。
语法:
SET autocommit = 1;
3. 事务隔离级别
作用:定义一个事务可能看到其他并发事务正在进行的未提交或已提交更改的程度,以在数据一致性和并发性能之间取得平衡。
常见的并发问题
- 脏读 (Dirty Read): 读取到其他事务未提交的修改。
- 不可重复读 (Non-Repeatable Read): 同一事务内,两次读取同一行数据得到不同结果(因其他事务提交了修改)。
- 幻读 (Phantom Read): 同一事务内,两次执行范围查询得到不同的行数(因其他事务提交了插入或删除)。
示例准备:
假设有 products 表:
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(50),
stock INT
);
INSERT INTO products (id, name, stock) VALUES (1, 'Laptop', 10);
我们将模拟两个并发会话(Session 1 和 Session 2)。
四种标准隔离级别详解
READ UNCOMMITTED(读未提交)
级别最低,隔离性最差,性能最好。允许脏读、不可重复读、幻读。一个事务可以看到其他事务尚未提交的更改。
脏读代码示例:
-- Session 1 (设置为 READ UNCOMMITTED)
SET SESSION transaction_isolation = 'READ UNCOMMITTED';
START TRANSACTION;
-- (稍后执行 SELECT)-- Session 2
START TRANSACTION;
UPDATE products SET stock = 5 WHERE id = 1; -- 修改库存,未提交-- Session 1
SELECT stock FROM products WHERE id = 1; -- 输出: 5 (读取到 Session 2 未提交的修改 - 脏读)-- Session 2
ROLLBACK; -- 回滚修改-- Session 1
SELECT stock FROM products WHERE id = 1; -- 输出: 10 (现在读到的是回滚后的值)
COMMIT;
评价:极少在生产环境使用。
READ COMMITTED(读已提交)
解决了脏读。允许不可重复读、幻读。一个事务只能看到其他事务已经提交的更改。每次SELECT读取当前已提交的最新版本。
不可重复读代码示例:
-- Session 1 (设置为 READ COMMITTED)
SET SESSION transaction_isolation = 'READ COMMITTED';
START TRANSACTION;
SELECT stock FROM products WHERE id = 1; -- 输出: 10 (第一次读取)-- Session 2
START TRANSACTION;
UPDATE products SET stock = 8 WHERE id = 1; -- 修改库存
COMMIT; -- 提交修改-- Session 1
SELECT stock FROM products WHERE id = 1; -- 输出: 8 (第二次读取,值变了 - 不可重复读)
COMMIT;
评价:许多数据库的默认级别,提供了基本的保障。
REPEATABLE READ(可重复读)
MySQL InnoDB 默认级别。 解决了脏读、不可重复读。理论上允许幻读(InnoDB 大多避免)。事务启动时创建数据快照 (Read View),普通SELECT读取快照,保证事务内多次读取相同行结果一致。
不可重复读 被解决 代码示例:
-- Session 1 (默认为 REPEATABLE READ 或显式设置)
-- SET SESSION transaction_isolation = 'REPEATABLE READ';
START TRANSACTION;
SELECT stock FROM products WHERE id = 1; -- 输出: 10 (第一次读取,基于事务开始时的快照)-- Session 2
START TRANSACTION;
UPDATE products SET stock = 8 WHERE id = 1;
COMMIT; -- Session 2 提交-- Session 1
SELECT stock FROM products WHERE id = 1; -- 输出: 10 (仍然读取快照中的值 - 可重复读)
COMMIT;
幻读 可能 示例 (及 InnoDB 的应对):
-- Session 1 (REPEATABLE READ)
START TRANSACTION;
SELECT COUNT(*) FROM products WHERE stock >= 10; -- 输出: 1 (假设只有 id=1 的 Laptop)-- Session 2
START TRANSACTION;
INSERT INTO products (id, name, stock) VALUES (2, 'Mouse', 15); -- 插入符合条件的新行
COMMIT; -- Session 2 提交-- Session 1
SELECT COUNT(*) FROM products WHERE stock >= 10; -- 输出: 1 (普通 SELECT 仍然看不到新插入的行,幻读被避免)-- 如果 Session 1 尝试更新或锁定读取:
-- UPDATE products SET stock = stock - 1 WHERE stock >= 10;
-- 此时 InnoDB 的 Next-Key Lock 会生效,Session 2 的 INSERT 可能会被阻塞,或者 Session 1 的 UPDATE 会更新到包括新插入的行(取决于时序和具体操作),但直接的“幻象”读取通常被 MVCC 避免了。COMMIT; -- Session 1 结束
InnoDB 通过 MVCC 和 Next-Key Locking 在很大程度上避免了幻读。
评价:在一致性和并发性之间提供了较好的平衡,是 MySQL 最常用的级别。
SERIALIZABLE(串行化)
级别最高,隔离性最强,性能最差。解决了所有并发问题。强制事务串行执行,通常通过加锁实现。
并发冲突与等待代码示例:
-- Session 1 (设置为 SERIALIZABLE)
SET SESSION transaction_isolation = 'SERIALIZABLE';
START TRANSACTION;
SELECT stock FROM products WHERE id = 1; -- 读取 id=1,加上读锁(共享锁)-- Session 2 (设置为 SERIALIZABLE)
SET SESSION transaction_isolation = 'SERIALIZABLE';
START TRANSACTION;
-- 尝试更新 id=1,需要写锁(排他锁)
UPDATE products SET stock = 8 WHERE id = 1; -- !!! 此操作会被阻塞,等待 Session 1 释放锁 !!!-- Session 1
-- ... (可以执行其他不冲突的操作)
COMMIT; -- 提交事务,释放锁-- Session 2
-- (阻塞解除,UPDATE 执行)
COMMIT; -- 提交事务
评价:提供最高一致性保证,但严重影响并发,仅在特定场景使用。
总结:隔离级别与并发问题
| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) | MySQL InnoDB 默认 |
|---|---|---|---|---|
| READ UNCOMMITTED | 允许 | 允许 | 允许 | 否 |
| READ COMMITTED | 不允许 | 允许 | 允许 | 否 |
| REPEATABLE READ | 不允许 | 不允许 | 理论允许 (InnoDB 大多避免) | 是 |
| SERIALIZABLE | 不允许 | 不允许 | 不允许 | 否 |
如何选择隔离级别?
取决于应用对一致性和性能的权衡:
- 高并发、可容忍轻微数据不一致(如报表):考虑
READ COMMITTED。 - 需要事务内读取一致性(大多数业务):
REPEATABLE READ(MySQL 默认) 是好选择。 - 要求绝对一致性,不惜牺牲并发:
SERIALIZABLE。 READ UNCOMMITTED:几乎不用。
语法回顾 (设置与查看)
-- 查看会话级别
SELECT @@transaction_isolation;
-- 设置会话级别
SET SESSION transaction_isolation = 'REPEATABLE READ';
-- 设置下一个事务级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
4. 事务控制总结表格
| 命令/概念 | 主要语法 / 变量名 | 描述 |
|---|---|---|
| 开始事务 | START TRANSACTION; / BEGIN; | 显式启动一个事务。 |
| 提交事务 | COMMIT; | 永久保存事务中的所有更改。 |
| 回滚事务 | ROLLBACK; | 撤销事务中的所有更改。 |
| 设置保存点 | SAVEPOINT name; | 在事务中创建一个可回滚到的标记点。 |
| 回滚到保存点 | ROLLBACK TO SAVEPOINT name; | 撤销到指定保存点之后的操作。 |
| 释放保存点 | RELEASE SAVEPOINT name; | 删除一个保存点(通常非必需)。 |
| 自动提交 | @@autocommit | 系统变量,控制是否自动提交每条语句 (1=是, 0=否)。 |
| 设置自动提交 | SET autocommit = 0/1; | 启用或禁用自动提交。 |
| 事务隔离级别 | @@transaction_isolation | 系统变量,显示当前隔离级别。 |
| 设置事务隔离级别 (会话) | SET SESSION transaction_isolation = 'level'; | 为当前会话设置隔离级别。 |
| 设置事务隔离级别 (下一个事务) | SET TRANSACTION ISOLATION LEVEL level; | 仅为下一个事务设置隔离级别。 |
练习题
假设有一个银行账户表 accounts (id INT PRIMARY KEY, name VARCHAR(50), balance DECIMAL(10, 2))。
-
编写一个事务,尝试将 ID 为 1 的账户的余额减少 200,并将 ID 为 2 的账户余额增加 200。如果两个操作都理论上能成功,则提交事务;如果任何一步看起来会失败,则回滚整个事务。
答案 (演示流程):START TRANSACTION; -- 开始事务 UPDATE accounts SET balance = balance - 200 WHERE id = 1; -- 尝试扣款 UPDATE accounts SET balance = balance + 200 WHERE id = 2; -- 尝试加款 -- 假设无误 COMMIT; -- 如果有误 -- ROLLBACK; -
在一个事务中,先插入一条新账户记录 (ID: 3, name: ‘Charlie’, balance: 1000),然后设置一个保存点
sp1。接着,尝试更新 ID 为 1 的账户余额为 500。如果更新成功,提交事务。如果更新失败(假设),则回滚到保存点sp1,然后提交事务(只保留插入操作)。
答案 (演示流程):START TRANSACTION; INSERT INTO accounts (id, name, balance) VALUES (3, 'Charlie', 1000); SAVEPOINT sp1; UPDATE accounts SET balance = 500 WHERE id = 1; -- 假设更新成功 COMMIT; -- ---- 或者,假设更新失败的场景 ---- -- START TRANSACTION; -- INSERT INTO accounts (id, name, balance) VALUES (3, 'Charlie', 1000); -- SAVEPOINT sp1; -- UPDATE accounts SET balance = 500 WHERE id = 999; -- 假设此操作失败 -- ROLLBACK TO SAVEPOINT sp1; -- COMMIT; -
查看当前的自动提交状态。
答案:SELECT @@autocommit; -
将当前会话的事务隔离级别设置为
READ COMMITTED。
答案:SET SESSION transaction_isolation = 'READ COMMITTED'; -
查看当前会话的事务隔离级别,确认是否已更改。
答案:SELECT @@session.transaction_isolation;
