RSS
菜单
全部文章快讯开发科技深度热点

第 3 章 事务与ACID面试精讲

内容摘要

事务是 MySQL 面试的第二大主题,几乎和索引并列必考。本篇用 18 道高频面试题,从 ACID 四大特性讲起,串起 redo log、undo log、binlog、两阶段提交、隐式提交、长事务、crash recovery 等核心考点。学完本篇,你能完整回答"事务怎么保证不丢数据、怎么保证不脏写",并接住"redo 和 binlog 为什么要两阶段提交"这类深挖追问。前置知识:会写基本的 INSERT/UPDATE;本系列示例基于 MySQL 8.0。

📖 本篇简介:事务是 MySQL 面试的第二大主题,几乎和索引并列必考。本篇用 18 道高频面试题,从 ACID 四大特性讲起,串起 redo log、undo log、binlog、两阶段提交、隐式提交、长事务、crash recovery 等核心考点。学完本篇,你能完整回答"事务怎么保证不丢数据、怎么保证不脏写",并接住"redo 和 binlog 为什么要两阶段提交"这类深挖追问。前置知识:会写基本的 INSERT/UPDATE;本系列示例基于 MySQL 8.0。

目录

  • 1 事务高频考点地图
  • 题目 1什么是事务ACID 四大特性分别指什么
  • 题目 2InnoDB 怎么实现原子性
  • 题目 3InnoDB 怎么实现持久性redo log 与 doublewrite
  • 题目 4InnoDB 怎么实现隔离性
  • 题目 5InnoDB 怎么实现一致性
  • 题目 6redo log 和 undo log 分别是干什么的
  • 题目 7什么是 autoCommit为什么建议显式事务
  • 题目 8事务的开启提交回滚怎么写SAVEPOINT 怎么用
  • 题目 9哪些语句会触发隐式提交
  • 题目 10长事务有什么危害怎么治理
  • 题目 11锁等待超时 innodblockwaittimeout 怎么调
  • 题目 12COMMIT 之后还能回滚吗ROLLBACK TO SAVEPOINT 呢
  • 题目 13本地事务与分布式事务的本质区别
  • 题目 14事务执行过程中宕机了会怎样crash recovery 流程
  • 题目 15binlog 三种格式 STATEMENTROWMIXED 的区别
  • 题目 16为什么需要 redo log 和 binlog 的两阶段提交
  • 题目 17undo log 什么时候清理purge 线程做什么
  • 题目 18事务里混了 DDL 或外部 RPC 会怎样
  • 事务速查表
  • 常见坑与注意事项
  • 复习清单
  • 练习题

1 事务高频考点地图

面试里"事务"类问题的常见问法,按难度分层:

| 难度 | 典型问题 | 本篇对应 |

|---|---|---|

| 入门 | 什么是事务?ACID 是什么? | 题目 1 |

| 基础 | redo/undo/binlog 各干什么?怎么提交? | 题目 2、3、6、15 |

| 进阶 | 两阶段提交?crash recovery?隐式提交? | 题目 14、16、9 |

| 实战 | 长事务治理?事务里加 RPC?锁等待? | 题目 10、11、18 |

一条主线记住:A 靠 undo log 回滚,D 靠 redo log 重做,I 靠锁 + MVCC,C 是前三者 + 业务约束的结果。所有追问都围绕这四条日志和隔离机制展开。

题目 1:什么是事务?ACID 四大特性分别指什么?

考点

  • 事务的定义:一组要么全成功要么全失败的 SQL 单元;
  • ACID 四个字母的准确含义;
  • 一致性是目标,其他三个是手段。

参考答案

  • 事务(Transaction)是数据库操作的最小逻辑单元,把多条 SQL 打包成"要么全部成功、要么全部回滚"的整体。
  • ACID:

1. A 原子性 Atomicity:事务内的操作不可分割,要么全做、要么全不做;

2. C 一致性 Consistency:事务前后数据库从一个合法状态转到另一个合法状态(约束、外键、业务规则不被破坏);

3. I 隔离性 Isolation:并发事务之间互不干扰,隔离级别决定干扰程度;

4. D 持久性 Durability:事务一旦提交,修改永久保存,宕机也不丢。

  • 关键认知:C 是目的,A、I、D 是数据库为了保证 C 提供的手段。面试时主动点出这层关系,会显得理解更透彻。
-- 经典转账事务(MySQL 8.0):扣 A 加 B 必须在同一事务
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;   -- 任一条失败则 ROLLBACK,钱不会凭空消失

追问

  • 问:MySQL 哪个存储引擎支持事务?答:InnoDB 支持,MyISAM 不支持,这也是 InnoDB 成为默认引擎的核心原因之一。
  • 问:一致性和隔离性什么关系?答:隔离性是保证一致性的手段之一;一致性还依赖主键、外键、CHECK 约束和业务代码。

题目 2:InnoDB 怎么实现原子性?

考点

  • undo log 的作用;
  • 回滚段(rollback segment);
  • 原子性与持久性的分工。

参考答案

  • 原子性靠 undo log(回滚日志) 实现:事务对数据做任何修改前,先把"修改前的旧值"写到 undo log;
  • 事务失败或显式 ROLLBACK 时,InnoDB 顺着 undo log 把数据反向回滚到修改前;
  • undo log 同时也是 MVCC 的基础——别的事务要读"历史版本"就读 undo log 里的旧值(详见第 6 章);
  • undo log 是逻辑日志,记录的是"做相反操作":INSERT 对应 DELETE,DELETE 对应 INSERT,UPDATE 对应写回旧值。
-- 原子性演示:故意制造失败,整条事务回滚
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 扣钱
SELECT balance FROM account WHERE id = 1;                 -- 会话内可见已扣
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '模拟异常';    -- 抛错
-- 事务自动回滚,id=1 的余额恢复原值

追问

  • 问:undo log 是物理日志还是逻辑日志?答:逻辑日志,记录反向 SQL 语义,不是"把数据块恢复成原样"。
  • 问:长事务为什么会让 undo log 暴涨?答:undo log 要保留到长事务结束才能被 purge,堆积后表空间膨胀,详见题目 17。

题目 3:InnoDB 怎么实现持久性?redo log 与 doublewrite

考点

  • redo log 是 WAL(Write-Ahead Logging);
  • redo log buffer → redo log file;
  • doublewrite buffer 解决 partial page write。

参考答案

  • 持久性靠 redo log(重做日志)+ WAL:修改数据时先写内存页(Buffer Pool),同时把"这次修改会把某页某偏移改成什么新值"写到 redo log;redo log 落盘后才返回 COMMIT 成功,数据页可以慢慢刷盘;
  • 宕机重启时,用 redo log 重放(redo)所有已提交但还没刷盘的修改,保证不丢;
  • doublewrite buffer:数据页 16KB 但操作系统块通常 4KB,宕机可能发生在写一半(partial page write),导致页本身损坏。InnoDB 先把脏页写到共享表空间的 doublewrite 区,再写到真实位置;恢复时若真实页损坏,用 doublewrite 的副本覆盖。
-- 查看 redo log 相关参数(MySQL 8.0)
SHOW VARIABLES LIKE 'innodb_log_buffer_size';     -- redo 内存缓冲
SHOW VARIABLES LIKE 'innodb_log_file_size%';     -- 单个 redo 文件大小
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';  -- 提交时刷盘策略,1 最安全

追问

  • 问:innodb_flush_log_at_trx_commit=0/1/2 区别?答:1=每次 commit 都刷盘(默认,最安全);2=commit 只写 OS cache,每秒刷盘(OS 崩丢 1 秒,MySQL 崩不丢);0=每秒刷盘(MySQL 崩丢 1 秒)。
  • 问:redo log 是循环写的吗?答:是,ib_logfile 固定大小循环复用,写满会触发 checkpoint 把脏页刷盘,否则阻塞业务。

题目 4:InnoDB 怎么实现隔离性?

考点

  • 锁(悲观)+ MVCC(乐观读);
  • 写靠锁、读靠 MVCC;
  • 隔离级别决定可见性。

参考答案

  • 隔离性由两套机制共同实现:

1. 写操作加锁:写一行就对该行加锁(记录锁/间隙锁/Next-Key Lock),阻止其他事务并发改同一行;

2. 读操作走 MVCC:普通 SELECT 不加锁,靠 undo log 构造历史版本快照,读到"自己该看到的版本";

  • 锁负责写写冲突,MVCC 负责读写并发,两者合起来让"读不阻塞写、写不阻塞读";
  • 隔离级别(读未提交/读已提交/可重复读/串行化)决定 MVCC 快照的生成时机和锁的范围,详见第 4 章。
-- 显式加锁的读(当前读),与普通快照读对比
START TRANSACTION;
SELECT * FROM account WHERE id = 1;             -- 快照读,MVCC,不加锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;  -- 当前读,加 X 锁,其他事务改不了
COMMIT;

追问

  • 问:为什么 RR 级别下还会幻读?答:InnoDB 靠 Next-Key Lock 在很大程度上避免了幻读,但快照读不加锁、当前读才靠锁,两者混用仍可能看到新行,详见第 4 章。
  • 问:SERIALIZABLE 怎么实现?答:所有普通 SELECT 自动变成 LOCK IN SHARE MODE,读写全部串行,性能差。

题目 5:InnoDB 怎么实现一致性?

考点

  • 一致性是最终目标;
  • 约束层:主键、唯一、外键、CHECK;
  • 一致性还依赖业务代码。

参考答案

  • 一致性 = 事务执行前后,数据库满足所有预设的完整性约束:

1. 数据库层:主键唯一、非空、外键引用合法、CHECK 约束、触发器;

2. 引擎层:A、I、D 保证事务不被中断、并发不脏写、提交不丢失;

3. 业务层:转账两边总额平衡、库存不为负——这些数据库管不着,要靠应用代码或约束。

  • 举例:A 扣 100、B 加 100,DB 保证这两步要么都成功要么都回滚;但"扣的是不是合法金额""余额够不够"是业务判断。
  • 面试时要说清:一致性不是数据库单方面能保证的,数据库只能保证"事务边界内不被破坏"。
-- 用 CHECK 约束把"余额非负"下沉到数据库(8.0.16+ 强制生效)
ALTER TABLE account ADD CONSTRAINT chk_balance CHECK (balance >= 0);
UPDATE account SET balance = balance - 100 WHERE id = 1;  -- 余额不足会报错

追问

  • 问:外键在高并发下推荐用吗?答:互联网高并发场景常拆掉外键,改由业务层维护引用完整性,避免外键检查带来的锁开销和级联更新。
  • 问:MySQL 8.0 的 CHECK 和 5.7 有啥区别?答:5.7 写了 CHECK 但被解析不生效(历史坑),8.0.16 起真正强制执行。

题目 6:redo log 和 undo log 分别是干什么的?

考点

  • redo:物理日志,崩溃恢复;
  • undo:逻辑日志,回滚 + MVCC;
  • 与 binlog 的分工(binlog 是 Server 层)。

参考答案

| 日志 | 层次 | 内容 | 作用 | 生命周期 |

|---|---|---|---|---|

| redo log | InnoDB 引擎层 | 物理页修改 | 崩溃恢复保持久性 | 循环写,checkpoint 后覆盖 |

| undo log | InnoDB 引擎层 | 反向操作逻辑 | 回滚 + MVCC 历史版本 | 事务结束 + purge 后清理 |

| binlog | Server 层 | 逻辑 SQL/行变更 | 主从复制、点恢复 | 归档文件,按过期策略清理 |

  • redo log 是物理日志("把 page 123 的偏移 4 改成 5"),顺序写、固定大小;
  • undo log 是逻辑日志("把 id=1 的 balance 改回 100"),保留多版本;
  • binlog 是 Server 层的逻辑日志,所有引擎共用,主要用于主从复制和备份恢复。
-- 查看 binlog 是否开启与格式
SHOW VARIABLES LIKE 'log_bin';            -- ON 表示开启
SHOW VARIABLES LIKE 'binlog_format';      -- ROW / STATEMENT / MIXED
SHOW BINARY LOGS;                          -- 当前所有 binlog 文件

追问

  • 问:为什么 InnoDB 还要有自己的 redo log,不能直接用 binlog?答:binlog 是 Server 层追加写、不支持崩溃时的"页级重做",且不是所有引擎都写;redo log 是 InnoDB 自己的 WAL,保证 crash recovery。
  • 问:redo log 写满会怎样?答:触发 forced checkpoint,把脏页刷盘,此时写入会变慢,这就是大促前要关注 redo 容量的原因。

题目 7:什么是 autoCommit?为什么建议显式事务?

考点

  • autocommit 默认 ON;
  • 单条 SQL 自动成事务;
  • 显式事务减少交互与日志开销。

参考答案

  • MySQL 默认 autocommit=ON:每条单独执行的 SQL 都是一个独立事务,执行完立即 COMMIT;
  • 显式 START TRANSACTION 开启后,直到 COMMIT/ROLLBACK 才算一个事务;
  • 为什么要显式事务:

1. 多条相关 SQL 必须原子化(转账、下单扣库存);

2. 减少 redo flush 次数:100 条 INSERT 在一个事务里只 flush 一次 redo,比 100 个独立事务快几十倍;

3. 减少网络往返和事务解析开销。

-- autocommit 默认开启:单条 SQL 自动提交
SHOW VARIABLES LIKE 'autocommit';   -- ON

-- 批量插入包成一个事务(性能天差地别)
START TRANSACTION;
INSERT INTO t_log VALUES (...);
INSERT INTO t_log VALUES (...);
INSERT INTO t_log VALUES (...);
COMMIT;

追问

  • 问:JDBC 里默认 autocommit 是啥?答:JDBC 默认也是 true,Spring @Transactional 会把它改成 false,方法结束再 commit。
  • 问:autocommit=OFF 会怎样?答:连接一旦不 commit 就一直挂着事务,容易长事务,生产一般保持 ON,靠显式 BEGIN/COMMIT 控制。

题目 8:事务的开启、提交、回滚怎么写?SAVEPOINT 怎么用?

考点

  • START TRANSACTION / BEGIN;
  • COMMIT / ROLLBACK;
  • SAVEPOINT 部分回滚。

参考答案

  • 开启:START TRANSACTION; 或 BEGIN;(二者等价,BEGIN 是别名);
  • 提交:COMMIT;;回滚整个事务:ROLLBACK;;
  • SAVEPOINT:在事务内打标记,可以 ROLLBACK TO sp_name 回滚到标记点,而不是整个事务回滚;适合"子操作失败但主流程继续"的场景。
-- SAVEPOINT 演示:部分回滚
START TRANSACTION;
INSERT INTO t_order VALUES (1, 'A', 100);
SAVEPOINT sp_before_detail;
INSERT INTO t_order_item VALUES (1, 1, '商品X');
UPDATE t_order SET status = 'PAID' WHERE id = 1;
-- 假设明细插入失败,但订单主单仍想保留:
ROLLBACK TO SAVEPOINT sp_before_detail;   -- 回滚明细,保留主单
RELEASE SAVEPOINT sp_before_detail;
COMMIT;

追问

  • 问:ROLLBACK TO SAVEPOINT 后事务结束了吗?答:没有,事务仍在,还要 COMMIT 或 ROLLBACK;只有 ROLLBACK 不带 TO 才结束事务。
  • 问:BEGIN 和 START TRANSACTION 完全等价吗?答:基本等价,START TRANSACTION 还能带 WITH CONSISTENT SNAPSHOT 等参数。

题目 9:哪些语句会触发隐式提交?

考点

  • DDL 自动提交;
  • 管理类语句自动提交;
  • 隐式提交会让"以为在事务里"的代码翻车。

参考答案

  • 以下语句会隐式 COMMIT 当前事务,面试高频坑:

1. DDL:CREATE/ALTER/DROP/TRUNCATE TABLE、CREATE INDEX 等;

2. 管理语句:GRANT/REVOKE、SET PASSWORD、CREATE USER;

3. LOCK TABLES / UNLOCK TABLES;

4. 某些 LOAD DATA、CREATE DATABASE 等。

  • 典型翻车:业务代码在事务里写了 TRUNCATE TABLE log,前面的修改被提前提交,后续回滚也救不回来。
-- 典型坑:事务里夹 DDL,前面的修改被隐式提交
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
TRUNCATE TABLE tmp_log;          -- ❌ 隐式 COMMIT,上面的 UPDATE 已落盘
-- 下面想 ROLLBACK 已经没用了
ROLLBACK;

追问

  • 问:TRUNCATE 为什么不能回滚?答:TRUNCATE 是 DDL(虽然部分 InnoDB 实现记 binlog),它会隐式提交并直接释放页,不走 DML 的 undo 流程,不要拿它当 DELETE 用。
  • 问:怎么避免?答:把 DDL 移出事务;事务内只放 DML;清理大表用 DELETE + 分批或 rename 方案。

题目 10:长事务有什么危害?怎么治理?

考点

  • undo log 堆积;purge 延迟;
  • 锁持有时间长;复制延迟;
  • 排查与治理手段。

参考答案

  • 长事务(运行时间很长的事务)的危害:

1. undo log 不能清理:长事务是"最旧活跃事务",purge 线程必须保留它之前的所有版本,导致 undo 暴涨、表空间膨胀;

2. 锁持有时间长:长事务持有的行锁/间隙锁阻塞其他事务,引发锁等待和死锁;

3. 复制延迟:主库长事务占着 gtid,从库追不上;

4. 连接池被占满。

  • 治理:

1. 用 information_schema.INNODB_TRX 找运行时间长的事务;

2. 设置 innodb_lock_wait_timeout 和 max_execution_time;

3. 业务上拆分大事务、把 RPC/HTTP 调用移出事务;

4. 监控 trx_running_time 并告警。

-- 找出当前运行超过 30 秒的事务
SELECT trx_id, trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS run_sec,
       trx_query
FROM information_schema.INNODB_TRX
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 30
ORDER BY trx_started;
-- KILL 掉对应连接:KILL <processlist_id>;

追问

  • 问:只读事务算长事务吗?答:算,RR 级别下只读事务也会阻止 purge,8.0 提供 SET TRANSACTION READ ONLY 优化只读事务的 undo 保留策略。
  • 问:事务里调用外部 HTTP 接口为什么危险?答:外部接口慢会把数据库连接拖成超长事务,接口抖动 = 数据库雪崩,务必把 RPC 移出事务边界。

题目 11:锁等待超时 innodb_lock_wait_timeout 怎么调?

考点

  • 锁等待不是死锁;
  • innodb_lock_wait_timeout 默认 50 秒;
  • 与死锁检测的区别。

参考答案

  • 两个事务争抢同一行锁,拿不到的一方会等待,直到对方提交或超时;
  • innodb_lock_wait_timeout 默认 50 秒,生产常调到 5~10 秒,避免连接被挂死;
  • 锁等待 ≠ 死锁:
  • 锁等待:单向等待,超时后报错 Lock wait timeout exceeded;
  • 死锁:循环互相等,InnoDB 主动死锁检测(innodb_deadlock_detect=ON)立即回滚代价小的事务。
-- 查看与调整锁等待超时(会话级)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
SET SESSION innodb_lock_wait_timeout = 5;   -- 会话级改成 5 秒
-- 系统级改 my.cnf:innodb_lock_wait_timeout = 5

追问

  • 问:死锁检测开着有什么副作用?答:高并发下每次加锁都要做"有向图环路检测",CPU 开销大;超热点场景可关检测 + 调小超时,但要谨慎。
  • 问:锁等待报错后业务怎么办?答:捕获异常、有限次重试(幂等前提)、或降级排队,不要无限重试。

题目 12:COMMIT 之后还能回滚吗?ROLLBACK TO SAVEPOINT 呢?

考点

  • COMMIT 是终态;
  • SAVEPOINT 的边界;
  • 人工误删后怎么救(binlog 闪回)。

参考答案

  • COMMIT 之后数据已持久化,不能 ROLLBACK。事务提交就是终点;
  • ROLLBACK TO SAVEPOINT 只回滚到标记点,事务仍然存活,需要再 COMMIT 或 ROLLBACK;
  • 一旦 COMMIT,想反悔只能靠:

1. binlog 闪回(把 ROW 事件反向生成 SQL);

2. 备份恢复(从最近全量 + binlog 重放到误操作前);

3. 延迟从库(提前部署一台延迟 1 小时的从库,误删后切过去)。

-- 演示:COMMIT 后 ROLLBACK 无效
START TRANSACTION;
UPDATE account SET balance = 0 WHERE id = 1;
COMMIT;                 -- 已落盘
ROLLBACK;               -- ❌ 此时没有活动事务,ROLLBACK 不报错但也不回滚

追问

  • 问:误 DELETE 全表怎么恢复?答:先别慌,立即 SHOW MASTER STATUS 记下位点,用 mysqlbinlog --start-datetime 导出误操作前的 binlog,或搭延迟从库;事后上线前先 BEGIN 包起来做 SQL Review。
  • 问:为什么生产强调"DELETE/UPDATE 先 SELECT 确认"?答:MySQL 没有 undo,commit 即终态,误操作成本极高。

题目 13:本地事务与分布式事务的本质区别?

考点

  • 本地事务的边界 = 一个数据库连接;
  • 跨库跨服务需要分布式事务;
  • 本篇只点到,第 18 章专讲。

参考答案

  • 本地事务:事务边界在单个数据库实例、单个连接内,靠 InnoDB 的锁 + WAL 保证 ACID;
  • 分布式事务:一次业务操作跨多个数据库/服务(如下单 = 订单库 + 库存库 + 账户库),本地事务管不着跨库的一致性;
  • 常见分布式方案:2PC/XA、TCC、本地消息表、Saga、最终一致性(MQ 事务消息);
  • 互联网主流选择:尽量避免强一致分布式事务,用业务拆分 + 最终一致性 + 补偿兜底。
-- MySQL 支持 XA 分布式事务(两阶段提交,了解即可)
XA START 'tx1';
UPDATE order_db.t_order SET status = 'PAID' WHERE id = 1;
XA END 'tx1';
XA PREPARE 'tx1';
XA COMMIT 'tx1';
-- 生产环境很少直接用 XA,性能差、阻塞强,详见第 18 章

追问

  • 问:Spring @Transactional 能跨库吗?答:单数据源下能;多数据源要 JTA/Atomikos 或改最终一致性方案,多数互联网项目用后者。
  • 问:为什么不推荐 XA?答:prepare 阶段锁资源时间长,吞吐骤降,一旦协调者挂掉容易阻塞。

题目 14:事务执行过程中宕机了会怎样?crash recovery 流程

考点

  • 启动时扫描 redo log;
  • 两阶段提交崩溃恢复;
  • 已 commit 的 redo 重放,未 commit 的 undo 回滚。

参考答案

  • MySQL 重启时做 crash recovery:

1. 从 checkpoint 开始扫描 redo log,重放所有"已写 redo 但脏页还没落盘"的修改(不管事务是否提交);

2. 扫描 undo log,把未提交的事务反向回滚;

3. 已提交的事务在 redo 里有完整记录,重放后数据不丢——这就是 D;

4. doublewrite 区负责修复写坏的页。

  • 一句话:redo 向前重做,undo 向后回滚,重启后数据库自动回到一致状态。
-- 观察 crash recovery 日志(错误日志)
-- 典型输出:
-- InnoDB: Doing recovery: scanned up to log sequence number 12345678
-- InnoDB: 1 transaction(s) which must be rolled back

追问

  • 问:crash recovery 慢怎么办?答:redo log 太大或脏页太多时恢复慢;8.0 引入 parallel doublewrite 和 page tracker 加速恢复。
  • 问:恢复期间能对外提供服务吗?答:不能,要等 recovery 完成才接受连接,所以大库崩溃恢复时间是高可用设计要考虑的。

题目 15:binlog 三种格式 STATEMENT/ROW/MIXED 的区别

考点

  • STATEMENT 记录 SQL;
  • ROW 记录行变更;
  • MIXED 自动选;
  • 8.0 默认 ROW。

参考答案

| 格式 | 记录内容 | 优点 | 缺点 |

|---|---|---|---|

| STATEMENT | 原始 SQL 语句 | 日志小、可读性好 | NOW()、UUID()、LIMIT 等在从库可能重放出不同结果 |

| ROW | 每行的前后镜像(before/after image) | 复制最精确、不会出错 | 日志体积大(批量 UPDATE 可能撑出几个 G) |

| MIXED | 自动选:默认 STATEMENT,函数类场景切 ROW | 折中 | 仍有边界 case,少用 |

  • MySQL 8.0 默认 ROW(5.7 之前默认 STATEMENT);
  • ROW 模式下闪回工具(binlog2sql)才有"反向 SQL"可用。
-- 切换 binlog 格式(需重启或动态设置)
SET GLOBAL binlog_format = 'ROW';
-- 查看当前 binlog 事件内容(用 mysqlbinlog 工具,8.0 推荐 --base64-output=DECODE-ROWS -v)
-- mysqlbinlog --base64-output=DECODE-ROWS -v binlog.000001

追问

  • 问:批量 DELETE 100 万行,ROW 模式 binlog 多大?答:每行都记前后镜像,可能几个 G,主从延迟会暴涨;这种批量操作要分批 + 限速。
  • 问:STATEMENT 模式下 UUID() 会怎样?答:主库执行一次生成一个 UUID,从库再执行 SQL 又生成另一个 UUID,数据不一致——这就是默认改 ROW 的原因。

题目 16:为什么需要 redo log 和 binlog 的两阶段提交?

考点

  • redo 是 InnoDB 层,binlog 是 Server 层;
  • 两阶段提交(prepare → commit);
  • 保证主从数据一致。

参考答案

  • 一个事务提交要写两份日志:InnoDB 的 redo log(保持久性)和 Server 层 binlog(复制/恢复用)。如果两份日志独立写,宕机时会出现:
  • redo 写了、binlog 没写 → 主库有这行、从库没这行,主从不一致;
  • binlog 写了、redo 没写 → 从库有这行、主库没有,更严重。
  • 两阶段提交(2PC)解决:

1. prepare 阶段:InnoDB 写 redo log 并置为 prepare 状态;

2. 写 binlog;

3. commit 阶段:InnoDB 把 redo 置为 commit 状态;

  • crash recovery 时:扫描到 prepare 状态的 redo,去 binlog 找有没有对应的 XID 事务——有则提交,没有则回滚。保证两份日志要么都在、要么都不在。
-- 验证两阶段提交状态:查看 redo prepare 事务
SELECT * FROM information_schema.INNODB_TRX;
-- 参数:binlog_group_commit_sync_delay 控制组提交延迟,高并发可调大提高吞吐
SHOW VARIABLES LIKE 'binlog_group_commit_sync_delay';

追问

  • 问:组提交是什么?答:多个事务在 prepare 后一起刷 binlog,减少 fsync 次数,是高并发写入的关键优化;binlog_group_commit_sync_delay 让组提交等更多事务凑批。
  • 问:不开 binlog 还需要两阶段提交吗?答:不需要两阶段,但 InnoDB 自己的 redo 仍是必须的,两阶段是为了配合 Server 层 binlog。

题目 17:undo log 什么时候清理?purge 线程做什么

考点

  • undo log 不是事务一结束就删;
  • purge 线程负责清理历史版本;
  • 最旧活跃事务(uplc)决定水位线。

参考答案

  • undo log 保留着行的历史版本,MVCC 要靠它给其他事务读老版本;
  • 清理条件:所有"早于该版本"的活跃事务都结束了,purge 线程才能把对应 undo 标记删除;
  • 这个"最旧活跃事务"叫做 uplc(undo purge watermark),长事务会把水位线拖住,undo 越堆越多;
  • purge 线程还负责:删除已标记删除的索引记录、清理 change buffer 等。
-- 查看 undo 与 purge 相关状态
SHOW ENGINE INNODB STATUS\G
-- 关注 History list length:越大说明 undo 堆积越严重
-- 经验:history list length 持续上涨 = 有长事务拖住

追问

  • 问:undo 暴涨导致 ibdata1 变大怎么办?答:ibdata 不会自动缩小;8.0 支持独立 undo 表空间 + innodb_undo_log_truncate=ON 自动 truncate,5.7 之前要 dump/import 重建。
  • 问:history list length 多少算高?答:持续 > 100 万就要排查长事务,> 千万已经影响性能。

题目 18:事务里混了 DDL 或外部 RPC 会怎样

考点

  • DDL 隐式提交;
  • RPC 把事务时间拉长;
  • 事务边界最佳实践。

参考答案

  • 事务里夹 DDL:任何 DDL 语句会隐式 COMMIT,前面的修改全部提前落盘,后面再 ROLLBACK 也回不去;
  • 事务里夹外部 RPC/HTTP/MQ:

1. 外部接口慢 → 数据库连接被占住 → 长事务 → undo 堆积 + 锁等待;

2. 外部接口成功但本地事务回滚 → 数据不一致(钱扣了订单没了);

  • 正确姿势:

1. 事务内只放必要的 DML;

2. RPC/MQ 放到事务外,配合本地消息表 / 事务消息做最终一致性;

3. DDL 永远在事务外执行,发版时单独跑。

// 错误示例:事务里调外部支付接口
@Transactional
public void createOrder(Order o) {
    orderDao.insert(o);
    payClient.charge(o);          // ❌ RPC 慢 = 长事务;RPC 成功但后续异常 = 不一致
}

// 正确姿势:先落库(本地消息表),事务提交后异步调支付
@Transactional
public void createOrder(Order o) {
    orderDao.insert(o);
    msgDao.insert(new Msg("PAY", o));  // 同事务写消息表
}
// 异步任务扫消息表调支付,成功后标记已发送

追问

  • 问:事务里能查缓存吗?答:可以,但要注意缓存读超时也会拖长事务;尽量把缓存读放到事务外组装数据。
  • 问:Spring @Transactional 自调用为什么失效?答:同类内部方法调 @Transactional 方法不走代理,事务注解不生效,这是 Spring 层的坑,不是 MySQL 的。

事务速查表

| 问题 | 标准答案 |

|---|---|

| A 靠什么 | undo log 反向回滚 |

| D 靠什么 | redo log + WAL + doublewrite |

| I 靠什么 | 锁(写写)+ MVCC(读写) |

| C 靠什么 | A/I/D + 约束 + 业务 |

| redo 是什么日志 | InnoDB 层物理日志,循环写 |

| undo 是什么日志 | InnoDB 层逻辑日志,purge 后清理 |

| binlog 在哪层 | Server 层逻辑日志,用于复制和闪回 |

| 两阶段提交 | prepare → 写 binlog → commit |

| 8.0 binlog 默认格式 | ROW |

| 锁等待默认超时 | 50 秒,生产建议 5~10 秒 |

| 长事务排查 | information_schema.INNODB_TRX |

| undo 水位线 | uplc = 最旧活跃事务 |

常见坑与注意事项

1. 以为 ROLLBACK 能救 COMMIT:commit 即终态,靠 binlog 闪回和延迟从库兜底,不要迷信"先跑了再说"。

2. 事务里 TRUNCATE/ALTER:隐式提交让回滚失效,DDL 必须挪出事务。

3. autocommit=ON 下写"假事务":代码里没 START TRANSACTION,多条 SQL 其实是各自独立提交,原子性根本不存在。

4. 长事务里跑大查询:SELECT 本身不改数据,但在 RR 级别下它就是活跃事务,照样拖住 purge。

5. binlog_format 改成 STATEMENT 图省空间:主从复制可能出现 NOW()、UUID() 不一致,8.0 默认 ROW 别乱改。

6. innodb_flush_log_at_trx_commit=2 图快:OS 崩丢 1 秒数据,金融场景必须保持 1。

7. 事务里 sleep/调试断点:本地调试忘了关断点,事务挂一夜,undo 暴涨。

复习清单

| 题目 | 核心结论 | 易错点 |

|---|---|---|

| ACID | A=undo, D=redo, I=锁+MVCC, C=目标 | C 是目的不是手段 |

| 原子性 | undo log 反向回滚 | 答成"redo" |

| 持久性 | WAL + redo 落盘 + doublewrite | 忘了 doublewrite 防半页写 |

| 隔离性 | 写锁读 MVCC | 答成"全靠锁" |

| 三日志分工 | redo/undo/binlog 层次不同 | 把 binlog 当成引擎层日志 |

| 隐式提交 | DDL/GRANT/LOCK TABLES 触发 | 事务里 TRUNCATE 翻车 |

| 长事务 | undo 暴涨 + 锁等待 + 复制延迟 | 只读事务也算 |

| 两阶段提交 | prepare→binlog→commit | 说不清 crash recovery 怎么配合 |

| binlog 格式 | 8.0 默认 ROW | STATEMENT 的函数不一致坑 |

| 事务边界 | 事务内只放 DML | 把 RPC/DDL 塞进事务 |

练习题

练 1:一笔事务里 UPDATE account SET balance = balance - 100 WHERE id=1; 执行成功但还没 COMMIT,此时数据库宕机。重启后 id=1 的余额会怎样?

<details>

<summary>参考答案</summary>

事务未 COMMIT,redo 虽然记录了修改,但 crash recovery 时扫描 undo log 会把这条未提交事务反向回滚,余额恢复原值。这正是 A(原子性)的体现:未提交的事务像没发生过。

</details>

练 2:线上一个订单事务,要扣库存、写订单、发 MQ 通知用户。请画出正确的事务边界,为什么 MQ 不能放在事务内?

<details>

<summary>参考答案</summary>

正确边界:BEGIN → 扣库存 → 写订单(同事务写一条待发送消息到本地消息表) → COMMIT;事务提交后由异步任务扫消息表发 MQ,发成功再标记。MQ 放事务内的问题:MQ broker 慢会拖长事务、broker 挂了事务回滚但用户已收到通知、本地回滚但 MQ 已发造成不一致。这就是"本地消息表 + 最终一致性"模式。

</details>

练 3:写出查看当前所有运行超过 1 分钟事务的 SQL。

<details>

<summary>参考答案</summary>

SELECT trx_id, trx_started,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS run_sec,
       trx_mysql_thread_id, trx_query
FROM information_schema.INNODB_TRX
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started;
-- 对应连接用 KILL trx_mysql_thread_id 终止

</details>

下一篇预告:第 4 章 隔离级别与并发问题面试精讲

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 3 章 事务与ACID面试精讲》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。