← [ BACK_TO_NOTES ]

BodySense 数据库第 13 课:PostgreSQL MVCC、Snapshot 与 Tuple Visibility

深入 PostgreSQL MVCC:row version、xmin/xmax、snapshot visibility、长事务与 dead tuple,并解释读写不互相普遍阻塞的原因。

#course #growing #backend #database #postgresql #bodysense

[!info] related notes

BodySense 数据库第 13 课:PostgreSQL MVCC、Snapshot 与 Tuple Visibility

[!abstract] 本课目标 把“MVCC = 多版本并发控制”从一句话变成能解释 PostgreSQL visibility 和 VACUUM 的系统模型。

从零建立概念:MVCC 到底在控制什么

MVCC = Multi-Version Concurrency Control,多版本并发控制。

关键不是“数据库会保存历史”这么宽泛。

它解决的是:

多个 transaction 同时读写时,不必为了让 Reader 看到一致数据,就把 Reader 和 Writer 全部互斥起来。

如果没有多版本,会怎样

最朴素的模型:

row X = 10

T1 reads X
T2 wants UPDATE X=20

如果只有“当前一个物理值”,为了避免 T1 读到半更新状态,可能需要非常强的 read/write lock。

读很多、写很多时会变成:

Reader waits Writer
Writer waits Reader

并发能力下降。

MVCC 改成:

old version X=10
new version X=20

不同 snapshot 根据 visibility rule 选择不同版本。

于是:

old reader
→ continues seeing X=10

new reader
→ after commit may see X=20

这就是“读写通常不互相阻塞”的核心原因。

Version 不是应用自己存的 revision column

BodySense 有:

BodyState.current_revision
BodyStateRevision.revision

这是业务层版本模型。

PostgreSQL MVCC tuple version 是数据库内部并发机制。

两者不要混淆:

Domain Revision
→ 业务历史 / optimistic expectation / audit

MVCC Version
→ PostgreSQL transaction visibility

业务可以同时利用两者。

xmin / xmax 的概念模型

PostgreSQL tuple header 会保存 transaction visibility 相关信息。

可以用简化模型理解:

xmin
→ 哪个 transaction 创建了这个 tuple version

xmax
→ 哪个 transaction 删除/替代/锁定了这个 version(具体语义更复杂)

不要把它简单当成应用时间戳。

它们服务于:

这个 snapshot 能不能看到这个 tuple version?

Snapshot 不是“数据库拷贝”

一个 snapshot 主要描述 transaction visibility 边界。

PostgreSQL 暴露的 snapshot 信息概念上包含:

xmin
xmax
currently-in-progress transaction IDs

执行器遇到 tuple version 时,根据:

creator committed?
creator visible to snapshot?
deleted by which tx?
deleter visible?

决定返回哪个 version。

UPDATE 在 MVCC 下发生什么

逻辑:

UPDATE users SET name='B' WHERE id='u1';

物理上不是简单:

find bytes 'A'
overwrite into 'B'

而可以理解成:

old tuple A
→ becomes old/dead for future snapshots after commit

new tuple B
→ created by updating transaction

仍活着的旧 snapshot 可以继续读 A。

DELETE 也不是立刻擦除 bytes

DELETE 的逻辑语义:

未来符合可见性规则的 transaction 不再看到这行。

物理旧 tuple 仍可能暂时存在,直到:

no relevant snapshot can need it
→ VACUUM can reclaim/reuse space

因此:

DELETE 1 million rows
→ table file size 不一定立刻缩小

这是 MVCC + VACUUM 语义,不是数据库忘了删。

Dead Tuple 是什么

简单说:

对未来正常 snapshot 已不再需要,但物理空间还等待 VACUUM 回收的旧 tuple version。

来源主要:

UPDATE
DELETE

高 churn table 会持续产生。

HOT Update:为什么某些 UPDATE 能少改索引

PostgreSQL 有 Heap-Only Tuple (HOT) optimization。

当 UPDATE 不改变被索引列、并且 page 有条件容纳新 tuple 时,数据库可以避免给所有索引新增普通 entry 链路。

这说明:

“UPDATE 一行”真实成本取决于改了哪些列、有哪些 Index、Page 是否有空间。

不需要现在背 HOT internals,但要知道写成本和 Index 设计是耦合的。

Long Transaction 为什么挡住清理

假设:

T1 starts snapshot at 10:00

10:01 ~ 11:00
many rows updated/deleted

如果 T1 仍然活着:

some old tuple versions
→ T1 might still need them
→ VACUUM cannot reclaim

所以 long-running / idle-in-transaction 不只占连接,还可能:

hold old snapshot
→ dead tuples retained
→ bloat
→ more pages
→ slower scans/vacuum

MVCC 和 Lock 的分工

错误理解:

有 MVCC 就不需要 Lock。

正确:

MVCC
→ readers choose visible versions
→ greatly reduce read/write blocking

Locks
→ coordinate conflicting writes
→ protect explicit critical sections
→ protect DDL/table metadata

例如两个 transaction 同时 UPDATE 同一 row,仍不能各自随便 commit 不协调。

下一课专门进入 Lock。

BodySense 里的两层版本很值得一起看

BodyState 使用:

PostgreSQL MVCC
+
SELECT ... FOR UPDATE
+
Domain current_revision
+
BodyStateRevision immutable history

它们分别解决:

MVCC
→ DB 内部并发可见性

FOR UPDATE
→ aggregate mutation serializes at current state row

current_revision / expectedRevision
→ domain-level stale write detection / history

revision table
→ durable business audit/replay

这是学习“数据库内部版本”和“业务版本”区别的非常好案例。

1. 为什么要多版本

如果 read 必须对所有 write 加排他互斥,读写并发会很差。MVCC 的核心办法:

UPDATE
→ 不让所有 reader 立刻跟着新值走
→ 创建新 row version
→ 不同 snapshot 看见不同 version

因此普通 reader 与 writer 很多时候不直接阻塞。

2. Tuple Visibility

概念上每个 tuple version 关联创建/失效事务信息。Snapshot 根据:

谁已经 commit?
谁仍 active?
版本是在 snapshot 前还是后?

判断这个版本对当前 statement/transaction 是否可见。

3. Long-running Transaction 的隐藏成本

只要一个很老 snapshot 仍可能需要旧版本:

VACUUM 不能安全清掉某些 dead tuples
→ bloat 增加
→ page/scan/index maintenance 成本上升

所以“一个只读事务不写东西”不代表没有系统代价。

4. MVCC 与锁不是二选一

MVCC 负责 snapshot visibility;真正写同一行、显式 FOR UPDATE、DDL 等仍然涉及 lock。

Prediction

[!question]

  1. PostgreSQL 中 reader 为什么通常不阻塞普通 writer?
  2. UPDATE 后旧 row version 为什么不能立刻删除?
  3. 一个打开数小时的 transaction 即使不写数据,为什么可能造成 bloat?
  4. MVCC 是否意味着数据库完全不需要 lock?
  5. Repeatable Read 为什么能持续看到一个稳定视图?

[!success]- Answer / 解析 1. Reader 通过 snapshot 读取对自己可见的 tuple version,writer 可以创建新版本,而不是要求 reader 全部等待。

2. 仍有旧 snapshot 可能需要它;只有确定再也没有 transaction 需要该版本后才能回收。

3. 老 snapshot 会抬高可清理边界,VACUUM 不能删除它仍可能看到的旧 version。

4. 不是。 写写冲突、显式锁、DDL、unique check 等仍依赖锁/冲突协议。

5. Transaction 使用稳定 snapshot,后续已提交的新 version 对该 snapshot 不可见。

Lab

两个 session:A 开 Repeatable Read 并查询;B UPDATE + COMMIT;A 再查询;然后检查 pg_stat_activity、tuple/vacuum 相关指标,观察老 transaction 生命周期。

L4 Gate

能把 UPDATE → 新旧 tuple version → snapshot visibility → dead tuple → VACUUM 连成一条因果链。

创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本