主题
MySQL 事务特性
一句话:InnoDB 把一组 SQL 包成事务,用 Undo 换原子性、用「隔离级别 + MVCC + 锁」换隔离性、用 Redo 换持久性;一致性不是第四个独立模块,而是这三者在业务约束下共同的结果。
适用版本:MySQL 8.0(InnoDB),默认隔离级别 REPEATABLE READ。8.4 的默认值未变,参数名以官方文档为准。
为什么需要
转账是最小例子:A 扣 100、B 加 100。如果两条 UPDATE 之间进程崩溃,或者第二条因为约束失败,数据就停在「扣了没加」的中间态。并发时还会遇到另一个问题:一个事务读到另一个事务未提交的中间值,对方一回滚,读到的数据从未存在过。
事务要同时解决三件事:
- 失败时能整体撤销,不留半成品。
- 并发事务之间看不到对方的中间状态。
- 提交返回成功后,即使机器立刻断电,数据仍在。
核心机制
原子性:Undo 记录旧值
每次修改前,InnoDB 把旧值写进 Undo Log。回滚时按 Undo 反向补偿;提交后,Undo 不再需要为回滚服务,但可能还要为 MVCC 的旧快照保留,等 Purge 线程确认没有读事务需要它才回收。
同一份 Undo 承担两个职责:回滚和版本链。这也解释了长事务的第一层代价——它让 Undo 既不能回滚也不能回收。
隔离性:隔离级别是策略,MVCC 和锁是执行者
| 隔离级别 | 读一致性 | 幻读 | 实现要点 |
|---|---|---|---|
| READ UNCOMMITTED | 读未提交 | 可能 | 基本不走 MVCC |
| READ COMMITTED | 每条 SELECT 新快照 | 可能 | 每次 SELECT 建 ReadView,间隙锁基本关闭 |
| REPEATABLE READ(8.0 默认) | 事务内快照复用 | 快照读避免,当前读靠 Next-Key Lock | 首次一致性读建 ReadView |
| SERIALIZABLE | 串行化 | 避免 | 普通 SELECT 也加共享锁 |
关键点:隔离级别本身不实现任何东西,它只决定 ReadView 何时创建、间隙锁是否启用。真正的读路径在 02-MVCC,写路径和当前读在 03-锁。
持久性:Redo 先落盘,数据页后刷
修改先写 Redo Log,再改 Buffer Pool 中的数据页;事务提交时按 innodb_flush_log_at_trx_commit 决定 Redo 何时 fsync。崩溃后用 Redo 重放「已提交但数据页没刷盘」的修改。
Redo 和 Binlog 的配合见 04-Redo 与 Binlog:两阶段提交保证两者要么都生效、要么都不生效。
一致性:是结果,不是开关
数据库能保证主键唯一、非空、外键、CHECK 等约束;「库存不能为负」「转账总额不变」属于业务不变量,需要应用层用事务边界、条件更新或唯一索引来兜。把一致性当成数据库的第四个功能,是常见的误解。
最小上手
两个会话验证 RR 的快照行为(MySQL 8.0):
sql
-- 会话 A
SELECT @@transaction_isolation; -- 期望 REPEATABLE-READ
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 第一次一致性读,建立 ReadView
-- 会话 B
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 回到会话 A
SELECT balance FROM account WHERE id = 1; -- RR:仍是旧值
COMMIT;
-- 会话 A 重新开事务后再查,才能看到新值回滚验证:
sql
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
ROLLBACK;
SELECT balance FROM account WHERE id = 1; -- 与事务前一致生产动作
innodb_flush_log_at_trx_commit=1(8.0 默认):每次提交 fsync Redo;改成 0/2 会用最多 1 秒的数据换吞吐。sync_binlog=1(8.0 默认):每次提交 fsync Binlog;与上一条合称「双 1」。transaction_isolation=REPEATABLE-READ(8.0 默认;5.7 的变量名是tx_isolation)。innodb_lock_wait_timeout=50(8.0 默认,单位秒):行锁等待超时后报Lock wait timeout exceeded,默认只回滚当前语句,事务本身还在,需要应用显式回滚。max_execution_time=0(8.0 默认,单位毫秒):只对只读 SELECT 生效,不能拿来给 UPDATE/DELETE 设超时。- 应用侧必须设事务超时(连接池/框架级),不要指望数据库参数兜住所有长事务。
长事务排查:
sql
SELECT trx_id, trx_state, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS seconds_open,
trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
WHERE trx_state = 'RUNNING'
ORDER BY trx_started;现象 → 命令 → 判断:磁盘不涨但 Undo 表空间持续变大,先查上面的视图;有小时级未提交事务就 kill 会话,并确认 SHOW ENGINE INNODB STATUS\G 的 History list length 是否开始下降。
边界
- 隔离级别不是越高越好:SERIALIZABLE 会把读也串行化,生产很少用;RC 在 ROW 格式 Binlog 下可以用,但会失去 RR 的快照复用,且当前读的幻读要靠业务或锁自己兜。
- 事务不是越短越好,而是边界要清楚:把 RPC、发消息、等用户输入放进事务,会把锁和 Undo 一起拖长;但为了「短」把该原子的一组写拆开,又会破坏一致性。
- 提交成功不等于业务成功:事务只保证数据库状态,消息是否发出、缓存是否更新,需要事务消息、Outbox 或 CDC 另行保证。
- DDL 不进事务:MySQL 的 DDL 隐式提交,不能和 DML 放在一个事务里回滚。
讲清楚
事务解决的是「一组写要么全成要么全败、并发时互不干扰、提交后扛得住崩溃」。InnoDB 用 Undo 记录旧值来实现回滚,同一份 Undo 还串成 MVCC 的版本链;用隔离级别决定 ReadView 何时创建、间隙锁是否启用,再由 MVCC 和锁分别处理快照读与当前读;用 Redo 的 WAL 保证已提交的修改能在崩溃后重放。一致性不是第四个独立能力,而是这些机制加上业务约束的结果。代价是 Undo 需要 Purge、长事务会拖住回收和锁,所以事务边界要短而清楚,但不能把该原子的一组写拆散。