这是《PostgreSQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 MVCC(Multi-Version Concurrency Control)让读不阻塞写、写不阻塞读,是 PostgreSQL 并发控制的核心。
15.1 行版本
每个行版本包含系统字段:
| 字段 | 含义 |
|---|---|
| xmin | 创建版本的事务 ID |
| xmax | 删除或更新该版本的事务 ID |
| ctid | 行版本物理位置 |
查看:
SELECT xmin, xmax, ctid, *
FROM users
WHERE id = 1;
15.2 可见性判断
UPDATE id=1
|
+-- old tuple: xmin=100, xmax=101
+-- new tuple: xmin=101, xmax=0
事务 100 之前的快照看到旧版本; 事务 101 之后提交后的快照看到新版本。
可见性取决于:
- 事务 ID;
- 提交状态;
- 事务快照;
- xmax 状态;
- clog 提交日志。
15.3 死元组
当旧版本对所有活跃快照不可见后,它成为死元组:
dead tuple
-> VACUUM 标记空间可复用
-> 后续插入或更新可复用
查看:
SELECT
relname,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
15.4 表膨胀
原因:
- 高频 UPDATE;
- 长事务;
- 复制槽堆积;
- autovacuum 配置不足;
- 索引过多;
- 大批量删除;
- 准备语句保持快照。
后果:
- 表和索引变大;
- 扫描变慢;
- 缓存效率降低;
- 统计和计划变差;
- 磁盘空间浪费。
15.5 快照保留
以下对象会保留旧版本:
- 长事务;
- 空闲事务;
- 慢查询;
- 逻辑复制槽;
- 备库反馈延迟;
- prepared transaction。
检查复制槽:
SELECT slot_name, slot_type, active, restart_lsn
FROM pg_replication_slots;
检查两阶段事务:
SELECT * FROM pg_prepared_xacts;
15.6 VACUUM 与 MVCC
VACUUM 不消除膨胀空间给操作系统,而是标记页内空间可复用。想缩减文件需要重建表或使用在线重建工具。
VACUUM users;
VACUUM FULL users;
VACUUM FULL 会重写表并请求 AccessExclusiveLock,生产慎用。
15.7 事务 ID 回卷
事务 ID 有限,需要冻结非常旧的事务。大量未冻结旧数据会触发激进 autovacuum,甚至阻止写入。
关注:
SELECT
relname,
n_dead_tup,
autovacuum_count,
last_autovacuum
FROM pg_stat_user_tables;
以及告警中的:
database is not accepting commands to avoid wraparound risk
必须提前治理长事务和 autovacuum。
15.8 优化高频更新
- 减少无效更新;
- 只更新变化字段;
- 拆分热点字段;
- 合并状态写放大;
- 使用 unlogged 或缓存过渡表;
- 调整填充因子;
- 优化 autovacuum;
- 考虑分区降低单表压力。
本章小结
MVCC 通过多版本实现高并发,代价是旧版本留在表中等待 VACUUM 清理。理解死元组、长事务、复制槽和表膨胀,是 PostgreSQL 生产运维的关键。
思考题
- xmin 和 xmax 分别表示什么?
- 为什么长事务危险?
- VACUUM 和 VACUUM FULL 的差异是什么?
- 表膨胀后如何恢复?
- 高频更新表如何降低写放大?