讲解
事务把多条操作打包成「要么全成功、要么全不做」的原子单元,即 ACID 里的 A(原子性);配合一致性、隔离性、持久性,构成 InnoDB 的可靠性基石。经典例子是转账:A 扣 100、B 加 100,任何一步失败都必须整体回滚,否则钱就凭空消失或产生。MySQL 默认 autocommit=1,每条语句自成一个事务;显式事务用 START TRANSACTION 开始、COMMIT 提交、ROLLBACK 回滚。
隔离级别决定并发事务之间能互相「看到」多少:READ UNCOMMITTED 会读到别人未提交的数据(脏读);READ COMMITTED 只读已提交的;REPEATABLE READ(MySQL 默认)保证同一事务内多次读结果一致,靠 MVCC 的 ReadView 实现;SERIALIZABLE 最强但近乎串行。互联网应用常把隔离级别调成 READ COMMITTED 以减少锁的范围,但要用 SELECT @@transaction_isolation 确认过再改。
锁是隔离性的执行手段。InnoDB 的行锁附着在索引记录上——WHERE 条件没走索引时,锁定的行数会远超预期甚至锁全表,这是「更新语句把库锁死」事故的常见根因。两个事务互相持有对方需要的锁就形成死锁,InnoDB 会自动检测并回滚其中一个(报 1213)。事务要尽量短:长事务持有锁、堆积 undo 日志、阻塞 purge,是生产环境的隐形杀手。SELECT ... FOR UPDATE 可以在事务内对读到的行加排他锁,配合事务实现「查出来改」的串行化。
示例
转账场景:两条 UPDATE 包在一个事务里,要么都生效要么都不生效:
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance DECIMAL(10,2) NOT NULL
);
INSERT INTO accounts VALUES (1, 1000.00), (2, 500.00);
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
SELECT * FROM accounts ORDER BY id;
回滚演示:事务内把余额清零,ROLLBACK 之后一切如旧:
START TRANSACTION;
UPDATE accounts SET balance = 0 WHERE id = 1;
SELECT balance FROM accounts WHERE id = 1;
ROLLBACK;
SELECT balance FROM accounts WHERE id = 1;
查看隔离级别,开一个事务并加行锁(FOR UPDATE),然后从 innodb_trx 观察这个活跃事务锁了几行:
SELECT @@GLOBAL.transaction_isolation AS global_level, @@SESSION.transaction_isolation AS session_level;
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
SELECT trx_id, trx_state, trx_rows_locked FROM information_schema.innodb_trx;
COMMIT;
常见坑
- 忘记 autocommit 是开着的:以为「多条语句天然是一个事务」,中间失败后留下半截数据。需要原子性就显式 START TRANSACTION。
- 长事务挂在线上等输入:事务开始后去做慢查询、调外部接口,锁一直不释放,把其他会话全部堵死。事务里只放必须原子执行的数据库操作。
- WHERE 没走索引导致锁放大:行锁锁的是索引记录,UPDATE 条件无索引时 InnoDB 只能锁更多行。UPDATE/DELETE 前先 EXPLAIN 确认走索引。
- DDL 混进事务:DDL 会隐式提交当前事务且不能回滚,事务里执行 ALTER 会把前面的修改提前提交,造成「回滚了但没完全回滚」。
小结
事务保证原子性,默认隔离级别 REPEATABLE READ 靠 MVCC 实现;行锁附着在索引上,条件不走索引会锁放大;事务要短,FOR UPDATE 显式加锁。下一章学习找出慢查询的入口:慢查询日志。