MySQL 的事务(Transaction)
1. 什么是事务?
事务是一组原子性的数据库操作序列,这些操作要么全部执行成功,要么全部失败回滚。事务的目的是确保数据库从一个一致状态转换到另一个一致状态,即使在执行过程中发生错误或中断。
典型场景:银行转账(A 转给 B 100 元)需要两个操作:
- A 的账户扣除 100 元。
- B 的账户增加 100 元。
这两个操作必须作为一个整体执行,否则会导致数据不一致。
2. 事务的 ACID 特性
事务必须满足 ACID 特性:
特性 | 描述 |
---|---|
原子性 (Atomicity) | 事务中的操作要么全部成功,要么全部失败回滚。 |
一致性 (Consistency) | 事务执行后,数据库从一个有效状态转换到另一个有效状态(如数据完整性约束)。 |
隔离性 (Isolation) | 并发事务之间互不干扰,每个事务感觉不到其他事务在同时执行。 |
持久性 (Durability) | 事务提交后,修改会永久保存到数据库(即使系统崩溃)。 |
3. 事务控制语句
MySQL 通过以下语句管理事务:
-
START TRANSACTION
或 BEGIN
:开启一个新事务。 -
COMMIT
:提交事务,确认所有修改。 -
ROLLBACK
:回滚事务,撤销所有未提交的修改。 -
SAVEPOINT
:在事务中设置保存点,用于部分回滚。
示例:
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user = 'A';
UPDATE accounts SET balance = balance + 100 WHERE user = 'B';
-- 如果两个操作都成功
COMMIT;
-- 如果失败
ROLLBACK;
4. 事务的隔离级别
多个事务并发执行时,可能引发以下问题:
问题 | 描述 |
---|---|
脏读 (Dirty Read) | 读取到其他事务未提交的数据。 |
不可重复读 (Non-Repeatable Read) | 同一事务内多次读取同一数据,结果不一致(数据被其他事务修改)。 |
幻读 (Phantom Read) | 同一事务内多次查询,结果集的行数不同(数据被其他事务新增/删除)。 |
MySQL 支持 4 种隔离级别(默认:REPEATABLE READ
):
隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
---|---|---|---|---|
READ UNCOMMITTED | ✔️ 可能 | ✔️ 可能 | ✔️ 可能 | 最高 |
READ COMMITTED | ❌ 避免 | ✔️ 可能 | ✔️ 可能 | 较高 |
REPEATABLE READ | ❌ 避免 | ❌ 避免 | ✔️ 可能 | 中等 |
SERIALIZABLE | ❌ 避免 | ❌ 避免 | ❌ 避免 | 最低 |
设置隔离级别:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
5. 事务的锁机制
MySQL 通过锁保证隔离性,常见的锁类型:
- 行锁(Row Lock):锁定某一行(InnoDB 默认)。
- 表锁(Table Lock):锁定整张表(MyISAM 默认)。
- 间隙锁(Gap Lock):锁定一个范围(防止幻读,在
REPEATABLE READ
级别中使用)。
示例:
- 事务 A 更新某行时,会对该行加锁,事务 B 必须等待锁释放才能操作。
6. 事务的实现原理
- 原子性:通过 Undo Log 实现回滚操作。
- 持久性:通过 Redo Log 确保数据持久化(即使崩溃后恢复)。
- 隔离性:通过 锁机制 和 MVCC(多版本并发控制) 实现。
- 一致性:由应用层和数据库约束(如主键、外键)共同保证。
7. 事务的常见应用场景
- 金融交易(转账、扣款)。
- 订单系统(创建订单、扣减库存)。
- 批量数据操作(保证多个操作原子性)。
8. 注意事项
- 避免长事务:长时间未提交的事务会占用锁资源,导致性能下降。
- 合理选择隔离级别:根据业务需求平衡一致性和性能。
- 监控死锁:使用
SHOW ENGINE INNODB STATUS
分析死锁。 - 重试机制:事务失败时,应用层应设计重试逻辑。
9. 示例:银行转账的完整流程
-- 设置隔离级别为 REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;START TRANSACTION;-- 检查 A 的余额是否足够
SELECT balance INTO @a_balance FROM accounts WHERE user = 'A' FOR UPDATE;IF @a_balance >= 100 THENUPDATE accounts SET balance = balance - 100 WHERE user = 'A';UPDATE accounts SET balance = balance + 100 WHERE user = 'B';COMMIT;
ELSEROLLBACK;
END IF;