跳到主要内容

事务与 MVCC

ACID 与实现手段​

答这题要把每个字母对应到具体机制,而不是只背定义:

特性靠什么实现
原子性 Atomicityundo log(回滚)
一致性 Consistency以上三者 + 业务约束共同保证
隔离性 Isolation锁 + MVCC
持久性 Durabilityredo 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 字节)—— 最后一次修改该行的事务 ID
  • DB_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 —— 下一个将分配的事务 ID
  • creator_trx_id —— 当前事务自己的 ID

可见性判断规则,拿行上的 DB_TRX_ID(记作 trx_id)逐条比:

  1. trx_id == creator_trx_id → 自己改的,可见
  2. trx_id < min_trx_id → 生成 ReadView 前就已提交,可见
  3. trx_id >= max_trx_id → 生成 ReadView 之后才开启的事务,不可见
  4. 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 是当前读,基于最新值。

长事务的危害​

面试爱追问「为什么要避免长事务」:

  1. undo log 无法回收 —— 只要有事务可能用到某个历史版本,对应 undo 就不能清理,ibdata 文件持续膨胀
  2. 版本链变长 —— 新事务要顺着长链找可见版本,查询变慢
  3. 锁长期不释放 —— 持有的行锁 / 间隙锁阻塞其他事务
  4. 主从延迟 —— 事务提交时才传 binlog

排查:information_schema.innodb_trx 按 trx_started 排序,找出跑了很久的事务。