这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 事务是 MySQL 成为交易系统事实来源的根基。它让一组操作要么全部可见,要么完全不可见,并在并发执行时为业务提供明确的一致性边界。
15.1 事务的 ACID
| 特性 | 含义 | 主要依赖 |
|---|---|---|
| Atomicity 原子性 | 事务内操作全部成功或全部失败 | Undo Log、事务管理 |
| Consistency 一致性 | 数据满足约束和业务规则 | 约束、锁、应用逻辑 |
| Isolation 隔离性 | 并发事务互不干扰 | MVCC、锁 |
| Durability 持久性 | 提交后的数据崩溃不丢 | Redo Log、Binlog、Doublewrite |
一致性是目的,原子性、隔离性和持久性是数据库提供的重要机制。应用把非法数据写入合法事务中,数据库无法替你保证业务一致性。
15.2 显式事务
开启事务:
START TRANSACTION;
等价写法:
BEGIN;
提交:
COMMIT;
回滚:
ROLLBACK;
示例:
START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE user_id = 1
AND balance >= 100;
UPDATE accounts
SET balance = balance + 100
WHERE user_id = 2;
COMMIT;
如果第二条更新失败,应执行:
ROLLBACK;
不能让一半转账留在数据库里。
15.3 autocommit
MySQL 默认开启自动提交:
SHOW VARIABLES LIKE 'autocommit';
autocommit=1 时,单条 SQL 没有显式开启事务也会自动提交。显式 START TRANSACTION 后,需要执行 COMMIT 或 ROLLBACK。
关闭自动提交:
SET autocommit = 0;
风险:
- 忘记提交会形成长事务;
- 连接归还连接池后事务状态可能残留;
- 锁和 Undo 版本长期保留;
- 主从延迟和性能问题难以定位。
应用建议:显式管理事务边界,提交或回滚必须放在 finally 逻辑中;连接归还前确认没有未结束事务。
15.4 SAVEPOINT
保存点允许部分回滚:
START TRANSACTION;
SAVEPOINT after_order;
INSERT INTO orders
(order_no, user_id, merchant_id, status, total_amount)
VALUES
('20260826000001', 1, 1, 'CREATED', 25.50);
SAVEPOINT after_coupon;
UPDATE coupons
SET used = 1
WHERE coupon_id = 100;
ROLLBACK TO SAVEPOINT after_coupon;
COMMIT;
结果:
- 订单插入保留;
- 优惠券更新被撤销;
- 事务仍未结束,直到 COMMIT。
删除保存点:
RELEASE SAVEPOINT after_coupon;
ROLLBACK TO SAVEPOINT 不是提交事务,只是撤销到某个内部位置。
15.5 并发异常
标准 SQL 定义了几个经典并发问题。
| 异常 | 描述 |
|---|---|
| Dirty Read 脏读 | 读到其他事务未提交的数据 |
| Non-repeatable Read 不可重复读 | 同一事务两次读取同一行值不同 |
| Phantom Read 幻读 | 同一条件两次读取出现或消失额外行 |
| Lost Update 丢失更新 | 两个事务基于旧值互相覆盖 |
示例表:
CREATE TABLE isolation_accounts (
id INT PRIMARY KEY,
balance DECIMAL(12, 2) NOT NULL
) ENGINE=InnoDB;
INSERT INTO isolation_accounts VALUES (1, 1000.00);
不可重复读
Session A:
START TRANSACTION;
SELECT balance FROM isolation_accounts WHERE id = 1;
Session B:
START TRANSACTION;
UPDATE isolation_accounts
SET balance = 900
WHERE id = 1;
COMMIT;
Session A 再读:
SELECT balance FROM isolation_accounts WHERE id = 1;
COMMIT;
如果两次结果不同,就是不可重复读。
15.6 四个隔离级别
查看级别:
SELECT @@global.transaction_isolation,
@@session.transaction_isolation;
设置会话级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
设置全局级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | InnoDB 常规情况下基本避免 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
MySQL InnoDB 默认是 REPEATABLE READ。
READ UNCOMMITTED
可能读到未提交数据:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
生产业务通常不使用。脏读会把中间状态、失败状态甚至回滚前的数据暴露给应用。
READ COMMITTED
每次一致性读都会生成新的 Read View,只能看到已提交数据。很多互联网系统将在线业务设置为 RC,原因是:
- 减少间隙锁使用;
- 并发写入冲突更少;
- 死锁概率下降;
- 业务多为单行短事务;
- 配合显式条件更新可满足一致性。
代价:
- 同一事务内两次读可能不同;
- 复杂业务需要显式锁定读;
- 不能依赖快照做业务判断。
REPEATABLE READ
事务第一次一致性读时创建 Read View,后续普通 SELECT 复用同一快照。
适合:
- 报表内多次读取希望口径一致;
- 默认 MySQL 行为;
- 需要减少不可重复读;
- 配合主键或唯一条件更新。
注意:
- 普通 SELECT 是快照读,不锁行;
- UPDATE 使用当前读;
- 先 SELECT 再 UPDATE 可能基于旧快照;
- 需要强一致判断时使用
SELECT ... FOR UPDATE; - 幻读不是在所有场景下都绝对不存在。
SERIALIZABLE
普通 SELECT 会被隐式转换为锁定读,并发能力显著下降:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
适合低并发、强隔离需求的任务。在线高并发交易系统很少使用。
15.7 快照读与当前读
快照读
普通 SELECT:
SELECT balance
FROM accounts
WHERE user_id = 1;
InnoDB 使用 MVCC 读取历史版本,不加行锁。
当前读
以下语句读取最新已提交或当前锁定版本,并涉及锁:
SELECT * FROM accounts WHERE user_id = 1 FOR UPDATE;
SELECT * FROM accounts WHERE user_id = 1 LOCK IN SHARE MODE;
UPDATE accounts SET balance = balance - 1 WHERE user_id = 1;
DELETE FROM accounts WHERE user_id = 1;
INSERT INTO accounts VALUES (...);
MySQL 8.0 中 FOR SHARE 替代 LOCK IN SHARE MODE,旧写法仍兼容。
经典问题:
START TRANSACTION;
SELECT stock FROM products WHERE id = 1;
-- 返回 10
UPDATE products SET stock = 5 WHERE id = 1;
如果另一个事务已经把库存改成 8,当前 UPDATE 会覆盖为 5。快照读不能防止丢失更新。
安全写法:
START TRANSACTION;
SELECT stock
FROM products
WHERE id = 1
FOR UPDATE;
UPDATE products
SET stock = 5
WHERE id = 1;
COMMIT;
或直接条件更新:
UPDATE products
SET stock = 5
WHERE id = 1
AND stock = 10;
15.8 丢失更新实验
创建表:
CREATE TABLE counters (
id INT PRIMARY KEY,
value INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO counters VALUES (1, 0);
错误流程:
-- Session A
START TRANSACTION;
SELECT value FROM counters WHERE id = 1; -- 读到 0
-- Session B
START TRANSACTION;
SELECT value FROM counters WHERE id = 1; -- 读到 0
UPDATE counters SET value = 1 WHERE id = 1;
COMMIT;
-- Session A
UPDATE counters SET value = 1 WHERE id = 1;
COMMIT;
B 的更新结果被 A 覆盖,等效丢失。
正确方式:
UPDATE counters
SET value = value + 1
WHERE id = 1;
或:
SELECT value FROM counters WHERE id = 1 FOR UPDATE;
15.9 事务边界设计
推荐:
开启事务
-> 校验和锁定必要数据
-> 少量核心写入
-> 提交
不应放入事务:
- 远程 HTTP 调用;
- 文件上传下载;
- 消息发送等待;
- 复杂规则计算;
- 用户交互等待;
- 大批量数据清洗;
- 锁内调用外部服务。
正确拆分:
1. 前置校验;
2. 开启本地事务;
3. 锁定并更新核心数据;
4. 提交事务;
5. 事务外发送消息或调用外部系统;
6. 通过状态机和对账处理外部失败。
15.10 查看事务状态
查看当前事务:
SELECT
trx_id,
trx_state,
trx_started,
trx_requested_lock_id,
trx_wait_started,
trx_rows_locked,
trx_rows_modified,
trx_mysql_thread_id
FROM information_schema.INNODB_TRX
ORDER BY trx_started;
查看长事务:
SELECT
trx_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds,
trx_mysql_thread_id
FROM information_schema.INNODB_TRX
WHERE trx_started < DATE_SUB(NOW(), INTERVAL 60 SECOND);
终止会话:
KILL <thread_id>;
终止前必须确认影响,不要把业务正在执行的正常大事务随意杀掉。
本章小结
事务通过 ACID 机制保证一组操作的可靠性。InnoDB 默认使用 REPEATABLE READ,普通 SELECT 是基于 MVCC 的快照读,UPDATE、DELETE 和锁定读是当前读。隔离级别是并发能力和一致性之间的取舍:RC 更适合高并发短事务,RR 提供更稳定的快照口径,SERIALIZABLE 则明显降低并发。
思考题
- ACID 分别解决什么问题?分别依赖哪些机制?
- RC 和 RR 的 Read View 创建时机有什么不同?
- 快照读和当前读的区别是什么?
- 为什么远程调用不应该放在数据库事务内?
- 设计一个余额转账事务,说明锁顺序、失败回滚和幂等处理。