跳到正文

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、长事务会拖住回收和锁,所以事务边界要短而清楚,但不能把该原子的一组写拆散。

把一门技术讲透到既懂也会用