news 2026/4/20 1:58:41

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/4/20 1:58:39

    基于SpringBoot+Vue的健康管理系统

    &#x1f345; 作者主页&#xff1a;Selina .a &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行交流合作。 主要内容&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据…

    作者头像 李华
    网站建设 2026/4/20 1:58:37

    【IEEE出版、快速EI检索】2026年人工智能、教育技术与应用国际学术会议(AIETA 2026)

    随着人工智能&#xff08;AI&#xff09;的迅速发展&#xff0c;其与教育的深度融合正在重塑全球教育生态系统。诸如智能辅导系统、个性化学习和教育大数据分析等创新应用为教育公平、质量提升和教学变革开辟了新的途径。为促进全球学者、教育工作者和技术专家之间的跨学科合作…

    作者头像 李华
    网站建设 2026/4/20 1:58:36

    A股大洗牌:六记重拳整顿量化交易,散户的春天来了?

    一场迟来的“正义”对于在A股市场中拼搏的普通散户而言&#xff0c;面对拥有顶级硬件和速度优势的高频量化交易&#xff0c;时常会有一种无力感和不公平感。然而&#xff0c;一场颠覆性的游戏规则大改已经落地。监管机构祭出组合重拳&#xff0c;旨在给那些靠技术优势在市场中“…

    作者头像 李华
    网站建设 2026/4/18 2:04:15

    双向链表是什么?和单向链表区别详解

    双向链表是数据结构中链表的一种重要形式&#xff0c;它在每个节点中不仅包含指向下一个节点的指针&#xff0c;还包含指向前一个节点的指针。这种设计使得双向链表在数据操作上比单向链表更加灵活&#xff0c;但也带来了额外的存储开销。在实际开发中&#xff0c;双向链表常用…

    作者头像 李华
    网站建设 2026/4/18 9:34:00

    Flutter艺术探索-Flutter Shader编程:着色器与特效实现

    Flutter Shader编程&#xff1a;用着色器打造炫酷特效 引言&#xff1a;不止于Widget的图形渲染 平时做Flutter开发&#xff0c;我们习惯用各种Widget堆叠界面&#xff0c;设置动画和样式——这能解决大部分视觉需求。但当你想要一个流动的动态背景、一种特殊的模糊效果&…

    作者头像 李华
    网站建设 2026/4/18 9:31:34

    基于Spring Boot的农产品直卖平台的设计与实现

    背景及意义 在乡村振兴战略深入推进与农业数字化转型加速的背景下&#xff0c;传统农产品流通模式因中间环节繁杂、信息不对称严重&#xff0c;常出现农民收益受损、消费者难获优质溯源农产品的双重困境&#xff0c;而现有农产品电商平台多存在功能模块零散、数据管理效率低、系…

    作者头像 李华