news 2026/2/14 22:35:59

PostgreSQL 核心原理:读不阻塞写,写不阻塞读的秘密

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL 核心原理:读不阻塞写,写不阻塞读的秘密

文章目录

    • 一、传统锁模型 vs MVCC:为什么需要多版本?
      • 1.1 传统锁模型的局限
      • 1.2 MVCC 的核心思想
      • 1.3 PostgreSQL 中 MVCC 的实现基础:元组头(HeapTupleHeader)
      • 1.4 事务快照(Snapshot):决定“你能看到什么”
      • 1.5 实践建议
    • 二、可见性判断:谁能看到这条记录?
      • 2.1 判断逻辑(简化版):
        • 步骤 1:检查 `t_xmin`(创建者)
        • 步骤 2:检查 `t_xmax`(删除者)
      • 2.2 举例说明
    • 三、解决“幻读”与“不可重复读”:隔离级别的实现
      • 3.1 Read Committed(读已提交)
      • 3.2 Repeatable Read(可重复读)
    • 四、MVCC 的代价:死元组与表膨胀
      • 4.1 什么是死元组(Dead Tuple)?
      • 4.2 VACUUM:MVCC 的“清道夫”
    • 五、高级优化:HOT(Heap-Only Tuple)更新
      • 5.1 HOT 的前提:
      • 5.2 HOT 的效果:
      • 5.3 MVCC 与 WAL、Checkpoint 的协同
    • 六、常见误区澄清
      • 6.1 误区 1:“MVCC 完全无锁”
      • 6.2 误区 2:“VACUUM 会立即释放磁盘空间”
      • 6.3 误区 3:“长事务只是占用连接”

PostgreSQL 之所以能在高并发场景下保持优异的性能和一致性,其核心秘密之一就是MVCC(Multi-Version Concurrency Control,多版本并发控制)。正是 MVCC 机制,使得 PostgreSQL 实现了“读不阻塞写、写不阻塞读”的理想并发模型——这是许多传统数据库(如 MySQL 的 MyISAM 或早期 Oracle)难以企及的能力。

本文将深入 PostgreSQL 的底层实现,从数据结构、事务可见性、元组版本链、快照机制到垃圾回收,全面解析 MVCC 如何工作,并揭示其背后的精巧设计与潜在代价。


一、传统锁模型 vs MVCC:为什么需要多版本?

PostgreSQL 的 MVCC 机制是其高并发能力的基石。它通过“保留历史版本 + 快照隔离”的方式,巧妙地实现了读写不互斥,同时保证 ACID 特性。然而,这种优雅的设计也带来了存储管理和维护的复杂性。

理解 MVCC 不仅有助于排查性能问题(如卡顿、膨胀),更能指导我们写出更高效的数据库应用。

1.1 传统锁模型的局限

在没有 MVCC 的数据库中(如使用两阶段锁 2PL),为了保证事务隔离性,通常采用以下策略:

  • 读操作:加共享锁(S 锁)
  • 写操作:加排他锁(X 锁)

这导致:

  • 写事务会阻塞所有读事务(直到提交)
  • 读事务会阻塞写事务(若持有 S 锁)

例如:一个长时间运行的SELECT查询会阻止其他会话对同一行执行UPDATE,造成严重的并发瓶颈。

1.2 MVCC 的核心思想

MVCC 的基本理念是:不覆盖旧数据,而是保留多个版本。每个事务看到的是它“应该看到”的那个版本,而不是最新版本。

读操作永远不需要加锁(只读快照)
写操作只需写入新版本,不影响正在读旧版本的事务

这种“时间旅行”式的视图,让读写完全解耦。


1.3 PostgreSQL 中 MVCC 的实现基础:元组头(HeapTupleHeader)

在 PostgreSQL 中,每一行数据(称为tuple)都存储在堆表(heap table)中,其物理结构包含一个关键部分:元组头(HeapTupleHeaderData)

typedefstructHeapTupleHeaderData{TransactionId t_xmin;/* 插入该 tuple 的事务 ID */TransactionId t_xmax;/* 删除/更新该 tuple 的事务 ID(0 表示未删除)*/CommandId t_cid;/* 命令 ID(用于同一事务内的多条语句区分)*/...uint16 t_infomask;/* 标志位:如 HEAP_XMIN_COMMITTED, HEAP_XMAX_INVALID 等 */...}HeapTupleHeaderData;

这些字段是 MVCC 可见性判断的核心依据。关键字段解释:

字段含义
t_xmin创建此元组的事务 ID
t_xmax删除或更新此元组的事务 ID(若为 0,表示未被删除)
t_cid同一事务内命令的序号(解决“自更新不可见”问题)
t_infomask优化标志位,缓存事务状态(如是否已提交)

注意:UPDATE在 PostgreSQL 中实际上是DELETE + INSERT—— 旧元组被标记为删除(t_xmax设为当前事务 ID),新元组被插入(t_xmin为当前事务 ID)。


1.4 事务快照(Snapshot):决定“你能看到什么”

每个事务在启动时(或首次访问数据时,取决于隔离级别)会获取一个事务快照(Snapshot)。这个快照定义了该事务在整个生命周期中“可见的数据世界”。

快照的组成(SnapshotData结构):

typedefstructSnapshotData{TransactionId xmin;// 最小活跃事务 ID(小于它的事务都已提交或回滚)TransactionId xmax;// 下一个将分配的事务 ID(大于等于它的事务尚未开始)TransactionId*xip;// 当前活跃事务 ID 列表(数组)uint32 xcnt;// 活跃事务数量...}

举个例子:

假设当前事务 ID 分配情况如下:

  • 已提交事务:100, 101, 102
  • 活跃事务:103(正在运行), 105(刚启动)
  • 下一个事务 ID 将是 106

那么一个在事务 104 中获取的快照可能是:

  • xmin = 103(因为 103 是最小的活跃事务)
  • xmax = 106
  • xip = [103, 105]

这意味着:

  • 事务 102 及之前的修改可见
  • 事务 103 和 105 的修改不可见(即使它们修改了数据)
  • 事务 106 及之后的修改尚未发生

1.5 实践建议

  1. 避免长事务:设置idle_in_transaction_session_timeout
  2. 合理配置 autovacuum:对高频更新表调低scale_factor
  3. 监控表膨胀:使用pg_bloat_check或查询pgstattuple
  4. 使用连接池:减少短连接带来的事务开销
  5. 谨慎使用SERIALIZABLE:虽然安全,但可能频繁回滚
  6. 定期 ANALYZE:确保统计信息准确,优化器选择高效计划

二、可见性判断:谁能看到这条记录?

PostgreSQL 使用函数HeapTupleSatisfiesVisibility()(实际由HeapTupleSatisfiesMVCC等实现)来判断某条元组对当前事务是否可见。

2.1 判断逻辑(简化版):

给定一个元组 T 和当前事务快照 S,判断 T 是否可见:

步骤 1:检查t_xmin(创建者)
  • 如果t_xmin >= S.xmax→ 元组在事务启动后才创建 →不可见
  • 如果t_xmin < S.xmin→ 创建者已结束:
    • t_xmin已提交 →可能可见
    • t_xmin已回滚 →不可见
  • 如果t_xminS.xip中(活跃事务)→不可见(未提交)
步骤 2:检查t_xmax(删除者)
  • 如果t_xmax == 0→ 未被删除 →可见
  • 如果t_xmax >= S.xmax→ 删除发生在事务启动后 →仍可见
  • 如果t_xmax < S.xmin→ 删除者已结束:
    • 若已提交 →不可见
    • 若已回滚 →可见
  • 如果t_xmaxS.xip中 →可见(因为删除未提交)

💡 这套逻辑确保了:只有已提交且在快照“之前”完成的修改才可见

2.2 举例说明

事务操作t_xmint_xmax
T1 (ID=100)INSERT row A1000
T2 (ID=101)UPDATE row A → B100101
INSERT row B1010
  • 事务 102(快照 xmin=102, xmax=102):

    • 看到 row A?→t_xmin=100 < 102,但t_xmax=101 < 102且已提交 →不可见
    • 看到 row B?→t_xmin=101 < 102且已提交 →可见
  • 事务 100 自己(在 UPDATE 后):

    • 能看到自己刚插入的 B 吗?能!因为t_xmin=101是自己,且在同一事务中通过t_cid区分命令顺序。

三、解决“幻读”与“不可重复读”:隔离级别的实现

PostgreSQL 支持四种 SQL 标准隔离级别,其中Read Committed(默认)Repeatable Read / Serializable的实现都依赖 MVCC。

3.1 Read Committed(读已提交)

  • 每次 SQL 语句执行时获取新快照
  • 同一事务中,两次SELECT可能看到不同结果(因为中间有其他事务提交)

3.2 Repeatable Read(可重复读)

  • 事务开始时获取一次快照,全程复用
  • 所有查询看到一致的数据视图

避免脏读、不可重复读
避免幻读(PostgreSQL 通过 MVCC + SSI 实现)

注意:PostgreSQL 的 Repeatable Read 实际上达到了 SQL 标准的Serializable级别(除了极少数边界情况),而真正的 Serializable 使用SSI(Serializable Snapshot Isolation)算法检测冲突并回滚。


四、MVCC 的代价:死元组与表膨胀

MVCC 并非免费午餐。其最大代价是:旧版本不会立即删除,导致存储膨胀

4.1 什么是死元组(Dead Tuple)?

当一个元组满足以下条件时,被称为“死元组”:

  • t_xmin对所有活跃事务都不可见(即创建者已提交,但被后续更新/删除)
  • t_xmax已提交(即删除操作已完成)

这些元组不再被任何事务需要,但仍然占据磁盘空间。

4.2 VACUUM:MVCC 的“清道夫”

PostgreSQL 通过VACUUM进程回收死元组:

  • 普通 VACUUM:标记死元组为空闲空间,供后续 INSERT 重用(不释放磁盘)
  • VACUUM FULL:重建表,真正释放空间(但会锁表,不推荐在线使用)

autovacuum守护进程会自动触发 VACUUM,基于以下阈值:

清理触发条件 = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * 表行数

若 autovacuum 跟不上写入速度,表会持续膨胀,I/O 和查询性能下降。


五、高级优化:HOT(Heap-Only Tuple)更新

为了减少索引更新开销和版本链长度,PostgreSQL 引入了HOT(Heap-Only Tuple)技术。

5.1 HOT 的前提:

  • 更新的列不在任何索引中
  • 新元组可以放入同一个数据页

5.2 HOT 的效果:

  • 新元组不创建新的索引项
  • 通过页内指针链接旧元组 → 新元组
  • 查询时沿 HOT 链遍历,直到找到可见版本

减少 WAL 日志量
减少索引维护开销
加速 UPDATE 性能

5.3 MVCC 与 WAL、Checkpoint 的协同

MVCC 与 WAL(Write-Ahead Logging)紧密配合:

  • 所有元组修改(包括t_xmin/t_xmax设置)都记录 WAL
  • 崩溃恢复时,通过 WAL 重放,重建正确的元组状态
  • Checkpoint 确保脏页刷盘,但不会影响 MVCC 可见性逻辑

此外,WAL 日志本身也包含事务提交/回滚记录,用于在恢复时判断t_xmin/t_xmax的最终状态。


六、常见误区澄清

6.1 误区 1:“MVCC 完全无锁”

  • 虽然读不加锁,但写操作仍需加轻量级锁(LWLock)保护共享结构
  • DDL(如ALTER TABLE)仍会加表级排他锁
  • 行级冲突(如两个事务同时 UPDATE 同一行)会导致一个等待

6.2 误区 2:“VACUUM 会立即释放磁盘空间”

  • 普通 VACUUM 只标记空间可重用,不会缩小表文件
  • 只有VACUUM FULLCLUSTERpg_repack能真正释放空间

6.3 误区 3:“长事务只是占用连接”

  • 长事务(尤其是idle in transaction)会阻止 VACUUM 清理死元组,导致表无限膨胀
  • 极端情况下可能触发事务 ID 回卷(Wraparound),使数据库进入只读模式

    版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
    网站建设 2026/2/8 16:53:26

    如何破解智慧养老“三大难题” ,惠及更多老年群体?

    要破解智慧养老“技术适配性差、服务供需错配、数字鸿沟显著”三大核心难题&#xff0c;需以老年人需求为中心。 通过技术适老化改造、服务精准化匹配、数字鸿沟弥合三大路径&#xff0c;结合政策引导、产业协同与社会参与&#xff0c;推动智慧养老从概念创新转向日常可用&…

    作者头像 李华
    网站建设 2026/2/10 14:25:43

    计算机网络应用层面试题(RPC)

    文章目录 RPC1. RPC的作用是什么&#xff1f;回答 2. [为什么有HTTP协议了&#xff1f;还要用RPC&#xff1f;](https://xiaolincoding.com/network/2_http/http_rpc.html#http-%E5%92%8C-rpc-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB)回答 RPC 1. RPC的作用是什么&…

    作者头像 李华
    网站建设 2026/2/14 21:08:16

    什么是Protobuf?一个例子比较Pb和JSON字节大小

    文章目录 什么是Protobuf&#xff1f;如何使用Protobuf &#xff1f;什么是 RPC应用程序之间的通信&#xff1f;Protobuf 和JSON 格式之间的区别是什么&#xff1f;Protobuf 的三个选项是什么&#xff1f;例子分别计算Pb和Json大小结语 什么是Protobuf&#xff1f; 你可能听说…

    作者头像 李华
    网站建设 2026/2/10 22:46:09

    AlertDialog.show()中message的字体大小和颜色如何修改?

    本问答帖原创发布在华为开发者联盟社区 &#xff0c;欢迎开发者前往论坛提问交流。 AlertDialog.show()中不能修改message里内容的字体颜色和大小,请问如何解决&#xff1f; 解决方案&#xff1a; AlertDialog无法修改自定义字体颜色和大小。建议使用coustomDialog&#xff0c…

    作者头像 李华