事务与 MVCC
ACID 与实现手段
答这题要把每个字母对应到具体机制,而不是只背定义:
| 特性 | 靠什么实现 |
|---|---|
| 原子性 Atomicity | undo log(回滚) |
| 一致性 Consistency | 以上三者 + 业务约束共同保证 |
| 隔离性 Isolation | 锁 + MVCC |
| 持久性 Durability | redo log(WAL) |
四种隔离级别
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 READ UNCOMMITTED | ✅ 会 | ✅ 会 | ✅ 会 |
| 读已提交 READ COMMITTED | ❌ | ✅ 会 | ✅ 会 |
| 可重复读 REPEATABLE READ | ❌ | ❌ | ⚠️ 基本解决 |
| 串行化 SERIALIZABLE | ❌ | ❌ | ❌ |
三个异常的区别要讲清:脏读读到别的事务未提交的数据;不可重复读同一行两次读值不同(别的事务 UPDATE 后提交了);幻读同一范围两次读行数不同(别的事务 INSERT 后提交了)。关键差异是不可重复读针对单行的值,幻读针对范围内的行数。
InnoDB 默认是可重复读(RR),而 SQL 标准推荐读已提交。历史原因是早期 binlog 只有 statement 格式,RC 下会导致主从数据不一致。注意 Oracle、PostgreSQL、SQL Server 默认都是 RC。
RR 下幻读解决了吗? 标准答案:快照读靠 MVCC 完全避免幻读;当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)靠间隙锁避免。但有一个经典漏洞场景:事务先快照读(没看到某行),再执行当前读或更新同一行,会「看到」其他事务插入的数据。所以严格说是「基本解决」。
MVCC 怎么工作
MVCC(多版本并发控制)的目标是读不加锁、读写不阻塞。三个组件:
1. 行的隐藏字段
每行除了用户数据,还有:
DB_TRX_ID(6 字节)—— 最后一次修改该行的事务 IDDB_ROLL_PTR(7 字节)—— 回滚指针,指向 undo log 里的上一个版本DB_ROW_ID(6 字节)—— 无主键时的隐藏主键
2. undo log 版本链
每次 UPDATE 都把旧值写一条 undo log,通过 DB_ROLL_PTR 串成从新到旧的链表。链上每个节点是该行在某个时刻的历史版本。
3. ReadView
事务做快照读时生成的「可见性快照」,含四个关键字段:
m_ids—— 生成时刻所有活跃(已启动未提交)事务的 ID 列表min_trx_id——m_ids里的最小值max_trx_id—— 下一个将分配的事务 IDcreator_trx_id—— 当前事务自己的 ID
可见性判断规则,拿行上的 DB_TRX_ID(记作 trx_id)逐条比:
trx_id == creator_trx_id→ 自己改的,可见trx_id < min_trx_id→ 生成 ReadView 前就已提交,可见trx_id >= max_trx_id→ 生成 ReadView 之后才开启的事务,不可见min_trx_id <= trx_id < max_trx_id→ 看是否在m_ids里:在 → 当时还活跃,不可见;不在 → 已提交,可见
不可见就顺 DB_ROLL_PTR 往版本链更旧的方向找,直到找到可见版本,或到链尾返回空(该行对本事务不存在)。
:::info RC 和 RR 的唯一区别 同一套 MVCC 机制,差别只在 ReadView 的生成时机:
- RC:每次 SELECT 都生成新的 ReadView → 能看到别的事务新提交的数据 → 不可重复读
- RR:只在事务内第一次 SELECT 时生成,之后复用 → 整个事务看到同一个快照 → 可重复读
这个对比是 MVCC 题的得分点。 :::
快照读与当前读
- 快照读(普通
SELECT):走 MVCC 读历史版本,不加锁 - 当前读(
SELECT ... FOR UPDATE/LOCK IN SHARE MODE/INSERT/UPDATE/DELETE):读最新版本并加锁
所以 RR 下有个反直觉现象:事务 A 快照读某行值为 10,事务 B 把它改成 20 并提交,A 再执行 UPDATE t SET v = v + 1,结果是 21 而不是 11 —— 因为 UPDATE 是当前读,基于最新值。
长事务的危害
面试爱追问「为什么要避免长事务」:
- undo log 无法回收 —— 只要有事务可能用到某个历史版本,对应 undo 就不能清理,
ibdata文件持续膨胀 - 版本链变长 —— 新事务要顺着长链找可见版本,查询变慢
- 锁长期不释放 —— 持有的行锁 / 间隙锁阻塞其他事务
- 主从延迟 —— 事务提交时才传 binlog
排查:information_schema.innodb_trx 按 trx_started 排序,找出跑了很久的事务。