跳到内容

12.2 InnoDB Read View、Undo 与加锁读


元数据卡

  • 前置:12.1 PostgreSQL MVCC、快照与隔离级别
  • 关键词:InnoDB、Read View、Undo、Consistent Read、Next-Key Lock、隔离选型
  • 代码语言:SQL(MySQL 8.4)

同样叫 Repeatable Read,PostgreSQL 与 InnoDB 的具体行为并不相同。应用迁移时,不能只搬隔离级别名称,还要区分普通快照读、当前读和加锁范围。

InnoDB 如何重建旧版本

InnoDB 在聚簇索引记录中维护事务标识和指向 undo 记录的回滚指针等隐藏字段。修改产生 undo 信息;一致性读需要旧版本时,会沿这些信息重建较早的行内容。

text
聚簇记录中的当前版本
  DB_TRX_ID = 120
  DB_ROLL_PTR ──> undo(旧值, 前一指针) ──> 更旧的 undo ...

undo 同时服务于事务回滚与一致性读。只要某个活跃 Read View 仍可能需要旧版本,相应 update undo 就不能被 purge。长事务因此会推高 history list、拖慢清理并扩大存储占用。

Consistent Read 与 Read View

普通 SELECT 在 Read Committed 和 Repeatable Read 下通常是非加锁一致性读。Read View 记录创建时活跃事务的边界与集合,读取器据此判断当前版本是否可见;不可见时沿 undo 重建更早版本。

  • Read Committed:每次一致性读建立新的 Read View;
  • Repeatable Read:同一事务的普通一致性读复用第一条一致性读建立的快照;
  • 本事务之前语句的修改对自己可见。

在 Repeatable Read 中,START TRANSACTION 后、第一次普通一致性读前,并发提交仍可能被首次快照包含。若业务要求立即确定快照,可在适用场景使用 START TRANSACTION WITH CONSISTENT SNAPSHOT

普通读与加锁读不是同一个时间点

sql
SELECT balance
FROM account
WHERE account_id = 1;

上面通常读取 Read View 中可见的历史版本,不因另一事务持有该行写锁就等待。

sql
SELECT balance
FROM account
WHERE account_id = 1
FOR UPDATE;

加锁读要锁定当前索引记录,必要时等待并读取较新的版本;旧版本无法被锁住。因此,同一 Repeatable Read 事务把普通快照读和加锁读混用时,可能观察到不同时间点的数据。设计读—判定—写流程时,应明确哪些读取只是报告,哪些读取必须保护后续修改。

FOR SHARE 获取共享锁,FOR UPDATE 获取更强的修改意图;NOWAIT 让冲突立即失败,SKIP LOCKED 跳过被锁行,后者适合多消费者任务队列,不适合需要完整一致结果集的通用查询。

Record、Gap 与 Next-Key Lock

InnoDB 锁定的是扫描遇到的索引记录和索引区间,而不是抽象的“SQL 行条件”。

  • record lock:锁某个索引记录;
  • gap lock:锁两个索引键之间的间隙,限制插入;
  • next-key lock:record lock 与其前方 gap 的组合;
  • insert intention:多个准备插入同一间隙不同位置的事务可表达各自意图。

在 Repeatable Read 下,加锁读和修改语句为了防止范围内出现幻行,可能使用 gap 或 next-key lock。若通过唯一索引的完整唯一条件定位单条记录,通常只需 record lock;缺索引或范围过宽会扩大扫描与锁范围。

Read Committed 通常关闭用于搜索和索引扫描的 gap locking,仅为外键检查、重复键检查等保留,并对不匹配行更早释放记录锁。它可能提升并发,也会改变应用依赖的范围保护。

MySQL 隔离级别不能照抄 PostgreSQL 表

InnoDB 默认隔离级别是 Repeatable Read,而 PostgreSQL 默认是 Read Committed。InnoDB 的 Read Uncommitted 允许 dirty read;这与 PostgreSQL 把该名称映射到 Read Committed 不同。

Serializable 在 InnoDB 中加强普通读取的锁行为,具体表现还受 autocommit 和语句形态影响。它不等于 PostgreSQL 的 SSI,也不应写成“Repeatable Read 的另一个名字”。

因此选型应从业务操作出发:

操作首选起点仍需确认
独立点查、原子增量Read Committed 或产品默认级别SQL 是否把判断放进原子更新
一致报表Repeatable Read 只读事务快照建立时点、长事务清理成本
读后修改固定行SELECT ... FOR UPDATE 或条件更新锁顺序、超时和死锁重试
维护范围不变量Serializable、范围锁或重新建模索引是否覆盖谓词、重试率
工作队列领取FOR UPDATE SKIP LOCKED跳过锁导致结果并非一致快照

一个不会静默覆盖的应用乐观锁

给记录增加版本号:

sql
ALTER TABLE account
ADD COLUMN version bigint NOT NULL DEFAULT 0;

UPDATE account
SET balance = 400.00,
    version = version + 1
WHERE account_id = 1
  AND version = 7;

应用必须断言影响行数为 1。影响 0 行表示版本已变化或目标不存在,应重新读取并决定重试、合并还是返回冲突;不能把它当作成功。

诊断顺序

遇到 InnoDB 阻塞时,按下面顺序提问:

  1. 这是普通一致性读、加锁读,还是写语句?
  2. 执行计划选择了哪个索引,扫描了多大范围?
  3. 当前隔离级别是否启用了范围保护?
  4. 阻塞的是 record、gap、next-key,还是元数据锁?
  5. 事务从何时开始,是否在等待应用或网络?

只看到 SQL 文本而不看索引访问路径,往往无法解释“明明只更新一行,为什么挡住了另一个插入”。

参考

下一章把 MVCC 背后的写入锁、两阶段锁、死锁检测和乐观验证放在一张图里比较。

Built with VitePress | Software Systems Atlas