PostgreSQLNotes

第 15 章:MVCC

zjc 于 2026-01-15 发布

这是《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 之后提交后的快照看到新版本。

可见性取决于:

  1. 事务 ID;
  2. 提交状态;
  3. 事务快照;
  4. xmax 状态;
  5. 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 表膨胀

原因:

  1. 高频 UPDATE;
  2. 长事务;
  3. 复制槽堆积;
  4. autovacuum 配置不足;
  5. 索引过多;
  6. 大批量删除;
  7. 准备语句保持快照。

后果:

  1. 表和索引变大;
  2. 扫描变慢;
  3. 缓存效率降低;
  4. 统计和计划变差;
  5. 磁盘空间浪费。

15.5 快照保留

以下对象会保留旧版本:

  1. 长事务;
  2. 空闲事务;
  3. 慢查询;
  4. 逻辑复制槽;
  5. 备库反馈延迟;
  6. 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 优化高频更新

  1. 减少无效更新;
  2. 只更新变化字段;
  3. 拆分热点字段;
  4. 合并状态写放大;
  5. 使用 unlogged 或缓存过渡表;
  6. 调整填充因子;
  7. 优化 autovacuum;
  8. 考虑分区降低单表压力。

本章小结

MVCC 通过多版本实现高并发,代价是旧版本留在表中等待 VACUUM 清理。理解死元组、长事务、复制槽和表膨胀,是 PostgreSQL 生产运维的关键。

思考题

  1. xmin 和 xmax 分别表示什么?
  2. 为什么长事务危险?
  3. VACUUM 和 VACUUM FULL 的差异是什么?
  4. 表膨胀后如何恢复?
  5. 高频更新表如何降低写放大?