BodySense 数据库第 13 课:PostgreSQL MVCC、Snapshot 与 Tuple Visibility
深入 PostgreSQL MVCC:row version、xmin/xmax、snapshot visibility、长事务与 dead tuple,并解释读写不互相普遍阻塞的原因。
[!info] related notes
- 课程 MOC:BodySense 数据库工程深化课程 MOC
- 前置:第 12 课
- 知识:MVCC、事务、并发与恢复总览
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]
- PostgreSQL 中 reader 为什么通常不阻塞普通 writer?
- UPDATE 后旧 row version 为什么不能立刻删除?
- 一个打开数小时的 transaction 即使不写数据,为什么可能造成 bloat?
- MVCC 是否意味着数据库完全不需要 lock?
- 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 连成一条因果链。