事务处理
事务处理
🌍 生活类比
想象一次银行转账——你从 A 账户转 1000 元到 B 账户:
- 从 A 账户扣除 1000
- 向 B 账户增加 1000
如果第 1 步成功、第 2 步失败(比如系统崩溃),A 的钱少了但 B 没收到——这就出大问题了。
事务就是把这两步绑定为一个"原子操作":要么两步都成功(COMMIT),要么两步都撤销(ROLLBACK),绝不会出现"半完成"的状态。
🎯 核心概念
什么是事务
事务是一组不可分割的 SQL 操作序列,要么全部成功执行,要么全部回滚撤销。它是数据库保证数据一致性的核心机制。
ACID 特性
| 特性 | 英文 | 含义 |
|---|---|---|
| 原子性 | Atomicity | 事务中的操作要么全部成功,要么全部回滚 |
| 一致性 | Consistency | 事务前后数据库从一个一致状态变到另一个一致状态 |
| 隔离性 | Isolation | 并发事务之间互不干扰,如同串行执行 |
| 持久性 | Durability | 事务提交后,数据永久保存,即使系统崩溃也不丢失 |
BEGIN / COMMIT / ROLLBACK
-- 开启事务
BEGIN; -- 或 START TRANSACTION
-- 执行一系列操作
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- 全部成功,提交
COMMIT;
-- 出现问题,回滚
ROLLBACK;
SAVEPOINT — 保存点
在事务内部设置"检查点",可以回滚到指定保存点,而不影响保存点之前的操作。
BEGIN;
INSERT INTO orders VALUES (1020, 101, 1, '2026-06-28', 500.00, 'pending');
SAVEPOINT sp1;
INSERT INTO order_items VALUES (1020, 1, 2, 100.00);
SAVEPOINT sp2;
-- 如果第二步出错,只回滚到 sp1
ROLLBACK TO sp1;
-- 保留第一步的操作
COMMIT;
| 命令 | 作用 |
|---|---|
SAVEPOINT name |
设置保存点 |
ROLLBACK TO name |
回滚到指定保存点(保留保存点之前的操作) |
RELEASE SAVEPOINT name |
删除保存点 |
事务隔离级别
当多个事务同时操作相同数据时,可能出现以下问题:
| 问题 | 说明 | 示例 |
|---|---|---|
| 脏读 | 读到其他事务未提交的数据 | 事务A修改了数据但未提交,事务B读到了修改后的值 |
| 不可重复读 | 同一事务内两次读取同一行,结果不同 | 事务A两次读同一行,中间事务B修改并提交了该行 |
| 幻读 | 同一事务内两次执行同一查询,行数不同 | 事务A两次查询,中间事务B插入了新行 |
SQL 定义了四个隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最低级别,几乎不用 |
| READ COMMITTED | ❌ 不会 | ✅ 可能 | ✅ 可能 | Oracle/PostgreSQL 默认 |
| REPEATABLE READ | ❌ 不会 | ❌ 不会 | ✅ 可能 | MySQL 默认 |
| SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 | 最高级别,性能最差 |
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
死锁
当两个事务互相等待对方释放资源时,就会发生死锁:
事务A:锁住表1 → 等待表2
事务B:锁住表2 → 等待表1
→ 互相等待,永远无法完成
数据库的处理方式:自动检测死锁,选择一个事务作为"牺牲者"进行回滚,另一个事务继续执行。
避免死锁的策略:
- 按固定顺序访问表和行
- 减少事务持有锁的时间
- 使用较低的隔离级别
- 为常用查询创建索引,减少锁范围
最佳实践
| 原则 | 说明 |
|---|---|
| 事务尽量短 | 减少锁持有时间,提高并发性能 |
| 不要在事务中等待用户输入 | 事务应快速完成 |
| 使用合适的隔离级别 | 不要默认用最高级别 |
| 统一访问顺序 | 减少死锁概率 |
| 加错误处理 | 出错时及时 ROLLBACK |
📝 基本语法
-- 开启事务
BEGIN; -- MySQL、PostgreSQL
START TRANSACTION; -- MySQL 也支持
-- 提交事务(所有操作永久生效)
COMMIT;
-- 回滚事务(撤销所有操作)
ROLLBACK;
-- 保存点
SAVEPOINT savepoint_name;
ROLLBACK TO savepoint_name;
RELEASE SAVEPOINT savepoint_name;
-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL 隔离级别;
-- 隔离级别:READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE
-- 设置全局隔离级别(影响所有新连接)
SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别;
-- 自动提交开关(MySQL 默认开启)
SET autocommit = 0; -- 关闭自动提交,需要手动 COMMIT
SET autocommit = 1; -- 开启自动提交(默认)
- MySQL 默认
autocommit = 1,每条 SQL 自动提交。使用事务前需先BEGIN或SET autocommit = 0 - DDL 语句(CREATE、ALTER、DROP)会隐式提交事务
- 连接断开时,未提交的事务会自动回滚
📌 示例
示例:订单创建事务(订单 + 库存扣减)
-- 场景:客户下单购买产品,需要同时创建订单和扣减库存
BEGIN;
-- 第一步:创建订单
INSERT INTO orders (order_id, customer_id, employee_id, order_date, total_amount, status)
VALUES (1020, 101, 1, '2026-06-28', 6999.00, 'pending');
-- 第二步:扣减库存
UPDATE products SET stock = stock - 1 WHERE product_id = 1;
-- 第三步:检查库存是否足够(如果 stock < 0 则回滚)
-- 在实际应用中,这一步通常用程序逻辑或触发器实现
-- 全部成功,提交
COMMIT;
如果中间出错:
BEGIN;
INSERT INTO orders (order_id, customer_id, employee_id, order_date, total_amount, status)
VALUES (1021, 102, 3, '2026-06-28', 129.00, 'pending');
UPDATE products SET stock = stock - 1 WHERE product_id = 2;
-- 假设发现库存不足,回滚整个事务
ROLLBACK;
-- 订单和库存都恢复到事务之前的状态
示例:使用 SAVEPOINT 部分回滚
BEGIN;
-- 第一笔订单:成功
INSERT INTO orders VALUES (1022, 103, 4, '2026-06-28', 3500.00, 'pending');
UPDATE products SET stock = stock - 1 WHERE product_id = 4;
SAVEPOINT after_first_order;
-- 第二笔订单:出错
INSERT INTO orders VALUES (1023, 104, 4, '2026-06-28', 2200.00, 'pending');
UPDATE products SET stock = stock - 1 WHERE product_id = 5;
SAVEPOINT after_second_order;
-- 发现第二笔订单有问题,回滚到第一笔之后
ROLLBACK TO after_first_order;
-- 第一笔订单仍然有效,提交
COMMIT;
-- 结果:订单1022被保留,订单1023被撤销
执行验证:
SELECT order_id, total_amount, status FROM orders WHERE order_id >= 1022;
order_id | total_amount | status
---------+-------------+--------
1022 | 3500.00 | pending
说明:ROLLBACK TO after_first_order 只撤销了保存点之后的操作,第一笔订单和库存扣减仍然保留。
🎬 场景演练
场景一:批量员工工资调整(带错误处理)
BEGIN;
-- 给销售部涨薪 10%
UPDATE employees
SET salary = salary * 1.10
WHERE department_id = (SELECT department_id FROM departments WHERE department_name = '销售部');
SAVEPOINT after_sales_raise;
-- 给技术部涨薪 8%
UPDATE employees
SET salary = salary * 1.08
WHERE department_id = (SELECT department_id FROM departments WHERE department_name = '技术部');
SAVEPOINT after_tech_raise;
-- 假设预算审批未通过,只撤销技术部涨薪
ROLLBACK TO after_sales_raise;
-- 销售部涨薪保留,提交
COMMIT;
要点:SAVEPOINT 实现"部分回滚",适合需要灵活控制的批量操作场景。
场景二:模拟转账并处理死锁
-- 会话A:从部门1转预算到部门2
BEGIN;
UPDATE departments SET budget = budget - 50000 WHERE department_id = 1;
-- 等待一会儿...
UPDATE departments SET budget = budget + 50000 WHERE department_id = 2;
COMMIT;
-- 会话B:同时从部门2转预算到部门1(另一个连接)
BEGIN;
UPDATE departments SET budget = budget - 30000 WHERE department_id = 2;
UPDATE departments SET budget = budget + 30000 WHERE department_id = 1;
COMMIT;
避免死锁的做法:
-- 两个会话都按 department_id 升序访问
-- 会话A:
BEGIN;
UPDATE departments SET budget = budget - 50000 WHERE department_id = 1; -- 先锁1
UPDATE departments SET budget = budget + 50000 WHERE department_id = 2; -- 再锁2
COMMIT;
-- 会话B:
BEGIN;
UPDATE departments SET budget = budget + 30000 WHERE department_id = 1; -- 也先锁1
UPDATE departments SET budget = budget - 30000 WHERE department_id = 2; -- 再锁2
COMMIT;
要点:统一访问顺序是避免死锁最简单有效的方法。
❓ 常见问题
Q:MySQL 默认是自动提交的,怎么使用事务? A: MySQL 默认
autocommit = 1,每条 SQL 都会自动提交。使用事务时,先执行BEGIN(或START TRANSACTION)开启事务,之后的 SQL 不会自动提交,直到你执行COMMIT或ROLLBACK。
Q:COMMIT 之后还能 ROLLBACK 吗? A: 不能。
COMMIT是事务的终点,提交后数据已永久保存,无法撤销。如果需要"撤销"已提交的数据,只能通过新的事务执行反向操作(如 UPDATE 回原值)。
Q:应该选择哪个隔离级别? A: 大多数场景用数据库默认级别即可(MySQL 默认
REPEATABLE READ,Oracle/PostgreSQL 默认READ COMMITTED)。只有在明确需要防止幻读且能接受性能损失时,才用SERIALIZABLE。
Q:DDL 语句(CREATE TABLE、ALTER TABLE)会受事务控制吗? A: 不会。DDL 语句会隐式提交当前事务。在事务中执行 DDL 后,之前的 SQL 无法再回滚。这是 MySQL 的行为,PostgreSQL 中 DDL 可以被回滚。
📖 小节
| 概念 | 说明 |
|---|---|
| 事务 | 一组不可分割的数据库操作 |
| ACID | 原子性、一致性、隔离性、持久性 |
| BEGIN | 开启事务 |
| COMMIT | 提交事务,永久生效 |
| ROLLBACK | 回滚事务,全部撤销 |
| SAVEPOINT | 事务内的保存点,支持部分回滚 |
| 隔离级别 | 控制并发事务之间的可见性 |
| 死锁 | 两个事务互相等待,数据库自动检测并回滚其中一个 |
- 事务尽量短,减少锁持有时间
- 按固定顺序访问资源,减少死锁
- 根据业务需求选择合适的隔离级别
📝 作业
- 编写一个事务:向
orders表插入一条新订单,同时扣减products表对应产品的库存。如果库存不足,回滚整个事务。 - 使用
SAVEPOINT实现一个事务中插入 3 条订单记录,然后回滚第 3 条,保留前 2 条。 - 查看你数据库的默认隔离级别,并尝试设置为
READ COMMITTED,观察对并发读写的影响。 - 思考:如果一个事务执行时间很长,会对其他事务产生什么影响?如何优化?
下一课
👉 22-stored-procedures - 存储过程:学习存储过程的创建和调用、输入输出参数、流程控制语句,以及存储过程在实际业务中的应用。



