目录

pgsql-22 MVCC多版本并发控制

1. 概述

MVCC(Multi-Version Concurrency Control,多版本并发控制)是现代数据库并发的基石,也是 PostgreSQL 面试出现频率最高的主题之一。它要解决的核心矛盾是:

如何让"读"和"写"互不阻塞?

传统的"读写加锁"方案下,一个事务在读某行时,其他事务不能写;写时不能读。并发一高就大量阻塞。MVCC 的思路是:每次修改都生成数据的一个新版本,读操作读取某个一致性时间点的旧版本。于是——

  • 读不阻塞写,写不阻塞读(读到的是历史版本)。
  • 每个事务看到的是一个一致的"数据库快照"。

PostgreSQL 和 MySQL(InnoDB) 都用 MVCC,但实现方式截然不同,这个差异是面试最爱考的点,本文会重点对比。

2. PostgreSQL 的 MVCC 实现

2.1 核心思想:多版本元组直接存在表里

PostgreSQL 最关键的设计:更新(UPDATE)不是原地修改,而是"标记旧行失效 + 插入新行"。同一行的多个版本(称为元组 tuple)直接存放在**堆表(heap)**中。

每个元组头部有几个隐藏系统字段:

系统列 含义
xmin 插入该版本的事务 ID(XID)
xmax 删除/更新该版本的事务 ID(0 表示未被删除)
ctid 该元组的物理位置(块号, 偏移)
cmin/cmax 同一事务内的命令序号
-- 可以直接查看隐藏系统列
SELECT xmin, xmax, ctid, * FROM accounts;

2.2 增删改如何操作版本

  • INSERT:写入新元组,xmin=当前事务IDxmax=0
  • DELETE:不真正删除,只把该元组的 xmax=当前事务ID(标记"我删了它")。
  • UPDATE = DELETE + INSERT:把旧版本 xmax=当前事务ID,同时插入一个新版本元组 xmin=当前事务ID

所以 PostgreSQL 的 UPDATE 会产生一个新元组,旧元组变成"死元组(dead tuple)"。这些死元组不会自动消失,需要 VACUUM与表膨胀 来回收——这正是 PG 特有的"表膨胀"问题的根源。

2.3 事务快照与可见性判断

每个事务/语句执行时会拿到一个快照(snapshot),快照记录了"此刻哪些事务是活跃的(未提交)"。核心信息:

  • xmin(快照):当前最小活跃事务 ID,小于它的事务都已结束。
  • xmax(快照):下一个将分配的事务 ID,大于等于它的都还没开始。
  • xip_list:正在进行中的活跃事务列表。

可见性规则(简化版):一个元组对当前快照可见,当且仅当:

  1. 它的 xmin 对应的事务已提交,且在快照看来是"过去"的;并且
  2. 它的 xmax 为 0,或 xmax 对应的事务未提交/已回滚(即它还没被有效删除)。
简言之:这个版本是由「已经提交且我能看见」的事务创建的,
        并且还没有被「已经提交且我能看见」的事务删除。

配合 t_infomask 里的提交状态标志位(Hint Bits)和 CLOG(事务提交日志)判断某个 XID 到底提交了没有。

2.4 HOT 更新(优化)

频繁 UPDATE 会产生大量死元组和索引更新。PostgreSQL 用 HOT(Heap-Only Tuple)优化:如果更新不涉及索引列,且新版本能放在同一个数据页内,就不需要更新索引——旧元组通过 ctid 链指向新版本,索引仍指向旧元组即可。

这大幅减少了索引膨胀。设计表时,把频繁更新的列排除在索引外能更好地触发 HOT。

3. 事务隔离级别

MVCC 是实现隔离级别的手段。PostgreSQL 支持标准的 4 个隔离级别:

隔离级别 脏读 不可重复读 幻读 PG 实际表现
Read Uncommitted ❌禁止 可能 可能 PG 中等同于 Read Committed(无脏读)
Read Committed(默认) 可能 可能 每条语句取新快照
Repeatable Read 事务开始时取一次快照;PG 下已无幻读
Serializable SSI 可串行化快照隔离
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- ... 整个事务用同一个快照 ...
COMMIT;

两个易考点:

  1. PG 默认是 Read Committed:每条 SQL 语句开始时都取一个新快照,所以同一事务内两次查询可能看到别的事务已提交的新数据(不可重复读)。
  2. PG 的 Repeatable Read 已经消除了幻读(快照隔离天然如此),这比 SQL 标准要求的更强;而 MySQL 靠间隙锁(Next-Key Lock)在 RR 下防幻读,机制完全不同。

3.1 Serializable(SSI)

PostgreSQL 的 Serializable 用 SSI(Serializable Snapshot Isolation,可串行化快照隔离),在快照隔离基础上检测"读写依赖环",发现可能破坏串行性时让其中一个事务失败回滚(报 could not serialize access)。它是乐观的——不加读锁,靠冲突检测。

4. PostgreSQL vs MySQL(InnoDB) 的 MVCC(面试核心)

这是本文最重要的对比,务必掌握:

维度 PostgreSQL MySQL / InnoDB
旧版本存哪 直接存在堆表里(新旧元组同表) 存在 undo log(回滚段)
UPDATE 方式 旧元组标记失效 + 插入新元组 原地更新,旧值写入 undo log
版本组织 表内多元组,靠 xmin/xmax + 可见性判断 行上有 DB_TRX_ID / DB_ROLL_PTR 指向 undo 版本链
回滚成本 极低(旧元组本就在,直接可见) 需要用 undo 反向恢复
旧版本清理 需要 VACUUM 异步清理死元组 Purge 线程清理不再需要的 undo
典型副作用 表膨胀(死元组堆积)、需要 VACUUM undo 膨胀(长事务导致 undo 无法回收)
回滚段争用 高并发下 undo 可能成瓶颈
可见性判断 元组头 xmin/xmax + 快照 + CLOG 行 trx_id 与 Read View 比较 + undo 回溯

一句话总结(面试金句):

PostgreSQL 把"旧版本"直接留在表里,代价是产生死元组、需要 VACUUM 回收,会有表膨胀;MySQL(InnoDB) 把"旧版本"放进 undo log 做版本链、原地更新,代价是回滚和一致性读要回溯 undo,长事务会撑大 undo。两者都用 MVCC 实现"读写不阻塞",只是"旧版本放哪、谁来清理"的取舍不同。

衍生问题:为什么 PG 的回滚很快,而 MySQL 回滚可能较慢? PG 回滚只需把当前事务创建的新元组标记为无效(它们本就不可见),几乎零成本;MySQL 回滚需要读 undo log 把数据一条条恢复原值。反过来,PG 为此付出的代价是需要 VACUUM。

5. 长事务的危害(PG 特别严重)

在 PG 中,只要有一个长时间不提交的事务,它的快照就"钉住"了一个旧的事务水平线(xmin),导致:

  1. VACUUM 无法清理那些"可能还被老事务看到"的死元组 → 表持续膨胀。
  2. 事务 ID 消耗加剧,逼近 **XID 回卷(wraparound)**风险。
-- 排查长事务/空闲事务(运维必备)
SELECT pid, state, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE state <> 'idle' AND xact_start IS NOT NULL
ORDER BY duration DESC;

结论:PG 中要极力避免长事务和"idle in transaction"(开了事务却迟迟不提交)。

6. 面试高频问答

Q1:什么是 MVCC?解决了什么问题? 多版本并发控制,通过为数据保留多个版本,让读操作读取一致性快照的历史版本,从而实现"读不阻塞写、写不阻塞读",大幅提升并发。

Q2:PostgreSQL 的 MVCC 和 MySQL 的有什么区别?(最高频) PG 把旧版本直接存在堆表里,UPDATE 是"旧元组失效+插入新元组",靠 xmin/xmax 和快照判断可见性,需要 VACUUM 清理死元组、会表膨胀;MySQL 把旧版本存在 undo log,原地更新形成版本链,靠 trx_id 和 Read View 判断可见性,由 purge 线程清理 undo。(见第4节表)

Q3:xmin 和 xmax 是什么? 元组头的系统字段:xmin 是创建该版本的事务 ID,xmax 是删除/更新该版本的事务 ID(0 表示未删除)。可见性判断就是比较它们与当前快照。

Q4:PG 的 UPDATE 为什么会导致表膨胀? 因为 UPDATE 不是原地改,而是把旧元组标记失效、插入新元组,旧元组变成死元组不会立即消失,累积就是表膨胀,需要 VACUUM 回收。

Q5:PG 默认隔离级别是什么?RR 下有幻读吗? 默认 Read Committed(每条语句取新快照)。PG 的 Repeatable Read 基于快照隔离,已经消除了幻读,无需像 MySQL 那样用间隙锁。

Q6:为什么长事务在 PG 中危害大? 长事务的旧快照会阻止 VACUUM 回收死元组,导致表持续膨胀,并加剧事务 ID 回卷风险。

7. 下一步

学习 锁机制详解,了解 MVCC 之外 PG 如何用锁保证一致性。