MVCC和快照
PostgreSQL 中的 MVCC(多版本并发控制)和 snapshot(快照),与之前介绍的锁一起,为事务提供了隔离性(I)保证。
它们算是老生常谈的话题了,相关资料非常多,可以先从 PG-JP book 看起。
简单说,MVCC 把目标数据沿时间维度拆成多个版本,而快照则决定了当前事务到底应该看到哪一个版本。如下图所示:
┌────────────────────────────── snapshot1
│
│ ┌─────────── snapshot2
▼ ▼
[---ver1---] [-ver2-] [----ver3----] row versions
MVCC
MVCC 主要用来解决读写冲突。在实现上,PG 采用了:
- 每次修改数据时,生成一个新版本的 tuple。
- 在 tuple header 中用
xmin/xmax记录事务 ID,从而界定该版本的“生存区间”。
除了记录事务 ID 之外,也可以像 DuckDB 一样用时间戳 / 全局时间线来实现多版本。
几种数据库系统中的实现如下:
| 数据库 | 实现方式 | 注释 |
|---|---|---|
| PostgreSQL | tuple: xmin/xmax,全量版本 + 带内存储,快照核心可概括为 xmin/xmax + 活跃 xid 列表 |
这里是简化描述;实际实现里还要考虑子事务、overflow 等细节,但核心仍然是不依赖全局时间戳 |
| MySQL/InnoDB | 聚簇数据 + undo log 版本链 | 与 PG 的思路有相似处,但物理组织方式明显不同,可参考InnoDB 原理 |
| DuckDB | tuple: ctime,增量 updateinfo,快照: stime |
引入了全局时间戳,因此不需要活跃事务列表;可参考这篇文章 |
PG 在多版本实现上的最大特点是:全量版本复制 + 带内存储。这既带来了不少优点,也引出了一系列后续问题,后面的 discussion 会再提。
Snapshot
仅有多版本还不够,还需要事务快照来决定“当前应该看到哪个版本”。这正是快照隔离类机制的核心。对 PostgreSQL 来说,REPEATABLE READ 可以近似理解为基于事务级快照的一致性读;而更强的 SERIALIZABLE 则是在此基础上通过 SSI 等机制进一步避免序列化异常。
之前我写的一篇详细的介绍:gp中的快照(含gp的分布式快照)。
需要注意的是,除了常规的 MVCC 快照之外,PG 里还有不少其他快照类型,见 HeapTupleSatisfiesVisibility():
bool HeapTupleSatisfiesVisibility(HeapTuple htup, Snapshot snapshot, Buffer buffer)
{
switch (snapshot->snapshot_type)
{
case SNAPSHOT_MVCC:
return HeapTupleSatisfiesMVCC(htup, snapshot, buffer);
case SNAPSHOT_SELF:
return HeapTupleSatisfiesSelf(htup, snapshot, buffer);
case SNAPSHOT_ANY:
return HeapTupleSatisfiesAny(htup, snapshot, buffer);
case SNAPSHOT_TOAST:
return HeapTupleSatisfiesToast(htup, snapshot, buffer);
case SNAPSHOT_DIRTY:
return HeapTupleSatisfiesDirty(htup, snapshot, buffer);
case SNAPSHOT_HISTORIC_MVCC:
return HeapTupleSatisfiesHistoricMVCC(htup, snapshot, buffer);
case SNAPSHOT_NON_VACUUMABLE:
return HeapTupleSatisfiesNonVacuumable(htup, snapshot, buffer);
}
...
在具体编码时,需要非常留意不同快照类型各自的适用场景。
Discussion
前面提到,PG 的 MVCC 实现方式很有特点,而很多针对 PG 性能的批评,根源也都在这里。相关讨论包括:
- Andy Pavlo 的 The Part of PostgreSQL We Hate the Most
its MVCC implementation is the worst among the other widely used relational DBMSs
- 字节’s 那些我很希望 Postgres 有,但 MySQL 已经有的功能
Postgres 基于堆的存储结构和元组版本的 MVCC 机制在写密集场景中会产生更多开销(写放大)。虽然 Postgres 通过 HOT 更新等特性有所改进,但在重写负载下仍会产生更多 WAL 流量,并需要更频繁的 VACUUM 操作。MySQL 则采用聚簇索引存储和撤销日志,这在写重负载下性能更好
这里再顺手提一下 PG 中事务实现的分工。事务需要满足 ACID,其中 C 更多是业务语义层面的要求;数据库系统实现时主要关注 A/I/D:
- A,原子性:通过事务标识和事务状态来保证。
- I,隔离性:通过 MVCC + 快照 + 锁来保证。
- D,持久性:通过 xlog / WAL 来保证。
另外,PG 也支持二阶段事务(2PC)。GP 的分布式事务,就是在自己实现协调者逻辑之后,复用了 PG 作为参与者的一部分事务机制。
Questions:
- PG 中游标和 DDL 操作的隔离性有些特殊,具体特殊在哪里?
看一下它们的快照
- PG 如何做高效的 online DDL 变更?
开放性问题,目前也没有特别理想的方案