news 2026/6/10 23:09:53

PostgreSQL 核心原理:读已提交与可重复读的底层实现差异(事务隔离级别)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PostgreSQL 核心原理:读已提交与可重复读的底层实现差异(事务隔离级别)

文章目录

    • 一、事务隔离级别概述:SQL 标准 vs PostgreSQL 实现
      • 1.1 SQL 标准定义的隔离级别问题
      • 1.2 关键差异对比表
      • 1.3 实践建议
    • 二、核心机制基础:MVCC 与事务快照(Snapshot)
      • 2.1 快照(Snapshot)是什么?
      • 2.2 可见性判断
    • 三、读已提交(READ COMMITTED):语句级快照
      • 3.1 快照获取时机
      • 3.2 行为示例
      • 3.3 底层实现细节
      • 3.4 优点与代价
    • 四、可重复读(REPEATABLE READ):事务级快照 + SSI 冲突检测
      • 4.1 快照获取时机
      • 4.2 行为示例
      • 4.3 底层实现:SSI(Serializable Snapshot Isolation)
      • 4.4 为什么 RR 能禁止幻读?
    • 五、监控与诊断
      • 5.1 查看当前事务隔离级别
      • 5.2 检测长 RR/SERIALIZABLE 事务
      • 5.3 监控 SSI 冲突(仅 SERIALIZABLE)
    • 六、底层代码路径简析(PostgreSQL 15+)
      • 6.1 快照获取逻辑(`src/backend/utils/time/snapmgr.c`)
      • 6.2 SSI 初始化(`src/backend/storage/lmgr/procarray.c`)
      • 6.3 可见性判断(`src/backend/access/heap/heapam_visibility.c`)
    • 七、常见陷阱与问题解决
      • 7.1 陷阱 1:误以为 RR 能避免所有并发问题
      • 7.2 陷阱 2:长事务导致膨胀

PostgreSQL 作为一款高度可靠的开源关系型数据库,其事务隔离机制是保障数据一致性和并发性能的核心支柱。在 SQL 标准定义的四种隔离级别中,读已提交(Read Committed)可重复读(Repeatable Read)是最常被使用的两种。尽管它们名称相似,但在 PostgreSQL 中的底层实现机制却存在根本性差异——这种差异直接影响了应用的行为、性能表现和一致性保证。

本文将深入 PostgreSQL 内核,从快照(Snapshot)获取时机、可见性判断逻辑、冲突检测机制等维度,全面剖析READ COMMITTEDREPEATABLE READ的实现原理,并揭示 PostgreSQL 如何通过MVCC + SSI(Serializable Snapshot Isolation)技术,在不牺牲性能的前提下提供强一致性保障。


一、事务隔离级别概述:SQL 标准 vs PostgreSQL 实现

PostgreSQL 的READ COMMITTEDREPEATABLE READ虽然只有一字之差,但其底层实现体现了两种不同的并发哲学:

  • READ COMMITTED:追求高吞吐与低延迟,接受“语句间视图漂移”,适合大多数 OLTP 场景。
  • REPEATABLE READ:追求事务内一致性,通过事务级快照 + SSI 依赖跟踪,不仅禁止不可重复读,还意外地禁止了幻读,成为 PostgreSQL 的“隐藏王牌”。

而这一切的背后,是 MVCC 与 SSI 的精妙结合——既避免了传统锁模型的阻塞开销,又提供了远超 SQL 标准的一致性保障。

记住:在 PostgreSQL 中,选择隔离级别不仅是选择“一致性强度”,更是选择“并发模型”。理解其底层机制,才能写出既正确又高效的数据库应用。

1.1 SQL 标准定义的隔离级别问题

隔离级别脏读不可重复读幻读
Read Uncommitted✅ 允许✅ 允许✅ 允许
Read Committed❌ 禁止✅ 允许✅ 允许
Repeatable Read❌ 禁止❌ 禁止✅ 允许
Serializable❌ 禁止❌ 禁止❌ 禁止

注:✅ 表示“可能发生”,❌ 表示“被禁止”

然而,PostgreSQL 并未完全遵循这一标准

  • 不支持READ UNCOMMITTED:最低级别即为READ COMMITTED
  • REPEATABLE READ实际禁止幻读:行为上等同于标准的SERIALIZABLE
  • 真正的SERIALIZABLE使用 SSI 算法,可能回滚事务以保证串行化

这种“超规格”实现,正是 PostgreSQL 并发控制先进性的体现。

1.2 关键差异对比表

特性READ COMMITTEDREPEATABLE READ
快照粒度每条 SQL 语句整个事务
不可重复读允许禁止
幻读允许禁止(PostgreSQL 特性)
SSI 依赖跟踪
冲突检测否(但记录依赖)
性能开销极低略高(需维护依赖图)
适用场景Web 应用、日志系统金融交易、报表统计
首次访问触发每次语句事务中第一次读/写

1.3 实践建议

场景推荐隔离级别理由
普通 Web 查询READ COMMITTED性能最优,足够安全
财务对账、报表REPEATABLE READ保证数据一致性
高并发金融交易SERIALIZABLE防止写偏斜,强一致性
批量数据导入READ COMMITTED减少快照开销

二、核心机制基础:MVCC 与事务快照(Snapshot)

要理解隔离级别的差异,必须先掌握 PostgreSQL 的MVCC(多版本并发控制)事务快照(Snapshot)机制。

2.1 快照(Snapshot)是什么?

快照是一个数据结构(SnapshotData),定义了当前事务“能看到哪些数据”。它包含三个关键字段:

TransactionId xmin;// 所有 < xmin 的事务已结束(提交或回滚)TransactionId xmax;// 所有 >= xmax 的事务尚未开始TransactionId*xip;// 当前活跃事务 ID 列表(未提交)

快照的本质是一个时间窗口:只有在此窗口“之前”已提交的修改才可见。

2.2 可见性判断

PostgreSQL 通过元组头中的t_xmin(创建事务)和t_xmax(删除/更新事务)结合快照,判断某条记录是否对当前事务可见。这是所有隔离级别的共同基础。


三、读已提交(READ COMMITTED):语句级快照

3.1 快照获取时机

  • 每次执行 SQL 语句时,重新获取一个新的快照
  • 即使在同一事务中,两次SELECT也可能看到不同结果

3.2 行为示例

-- 会话 ABEGIN;SELECTbalanceFROMaccountsWHEREid=1;-- 返回 100-- 此时会话 B 执行:-- UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT;-- 会话 A 继续:SELECTbalanceFROMaccountsWHEREid=1;-- 返回 200!COMMIT;

✅ 第二次查询看到了会话 B 已提交的修改
⚠️ 这就是“不可重复读”——被 SQL 标准允许,但在某些业务场景中是危险的

3.3 底层实现细节

  • 每次调用ExecutorStart()(执行器启动)时,若当前快照为空,则调用GetSnapshotData()获取新快照
  • 对于UPDATE/DELETE,目标行的可见性判断使用当前语句快照,但写入的新元组t_xmin为当前事务 ID
  • 写操作不会阻塞读,因为读的是旧版本

3.4 优点与代价

优点代价
并发度高,响应快同一事务内数据视图不一致
无额外冲突检测开销不适用于需要强一致性的场景(如转账)

四、可重复读(REPEATABLE READ):事务级快照 + SSI 冲突检测

4.1 快照获取时机

  • 事务首次访问数据时(通常是第一条 SQL 执行时)获取一次快照
  • 整个事务生命周期复用该快照
  • 所有查询看到完全一致的数据视图

4.2 行为示例

-- 会话 ABEGINTRANSACTIONISOLATIONLEVELREPEATABLEREAD;SELECTbalanceFROMaccountsWHEREid=1;-- 返回 100-- 会话 B:-- UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT;-- 会话 A 继续:SELECTbalanceFROMaccountsWHEREid=1;-- 仍返回 100!COMMIT;

✅ 两次查询结果一致 →禁止不可重复读
✅ 即使会话 B 插入新行,会话 A 的SELECT COUNT(*)也不会变化 →禁止幻读

📌 这是 PostgreSQL 对 SQL 标准的“增强”:RR 级别实际达到了 Serializable 的效果(除极少数情况)

4.3 底层实现:SSI(Serializable Snapshot Isolation)

从 PostgreSQL 9.1 开始,REPEATABLE READSERIALIZABLE都基于SSI 算法实现,区别仅在于是否启用冲突检测

隔离级别快照类型是否记录读写依赖是否检测冲突冲突时行为
READ COMMITTED语句级
REPEATABLE READ事务级允许提交
SERIALIZABLE事务级回滚事务

SSI 的核心思想:

  1. 记录“危险结构”(Dangerous Structures)

    • 事务 A 读取某行
    • 事务 B 修改该行并提交
    • 事务 A 后续又写入相关数据
      → 形成“读-写-写”依赖链,可能导致非串行化结果
  2. 构建序列化图(Serialization Graph)

  3. 检测环(Cycle):若有环,则存在不可串行化调度

REPEATABLE READ下,PostgreSQL记录依赖但不检测环,因此不会回滚,但能防止幻读;
SERIALIZABLE下,检测环并回滚,提供严格串行化。

4.4 为什么 RR 能禁止幻读?

传统数据库通过范围锁(Range Lock)防止幻读,但会严重降低并发。

PostgreSQL 的做法更巧妙:

  • 由于使用事务级快照,所有查询都基于同一时间点
  • 即使其他事务插入新行,只要其t_xmin >= snapshot.xmax,就不可见
  • 对于UPDATE/DELETE影响“未来行”的情况,SSI 会跟踪谓词(predicate)依赖

例如:

-- 事务 A (RR)SELECT*FROMordersWHEREstatus='pending';-- 返回 0 行-- 事务 BINSERTINTOorders(status)VALUES('pending');COMMIT;-- 事务 AUPDATEordersSETpriority=1WHEREstatus='pending';-- 影响 0 行

即使事务 B 插入了匹配行,事务 A 的UPDATE也不会影响它——因为该行在快照中不可见。这本质上消除了幻读。


五、监控与诊断

5.1 查看当前事务隔离级别

SHOWtransaction_isolation;-- 或SELECTcurrent_setting('transaction_isolation');

5.2 检测长 RR/SERIALIZABLE 事务

SELECTpid,query,xact_start,now()-xact_startASxact_age,wait_event_type,wait_eventFROMpg_stat_activityWHEREstate<>'idle'AND(current_queryLIKE'%REPEATABLE READ%'ORcurrent_queryLIKE'%SERIALIZABLE%')ORDERBYxact_ageDESC;

5.3 监控 SSI 冲突(仅 SERIALIZABLE)

SELECT*FROMpg_stat_database_conflictsWHEREdatname=current_database();-- 查看 serialization_failures

六、底层代码路径简析(PostgreSQL 15+)

6.1 快照获取逻辑(src/backend/utils/time/snapmgr.c

SnapshotGetTransactionSnapshot(void){if(IsolationUsesXactSnapshot())returnGetSnapshotData(&CurrentSnapshotData);elsereturnGetLatestSnapshot();// READ COMMITTED 走这里}
  • IsolationUsesXactSnapshot()返回 true 当且仅当隔离级别 ≥REPEATABLE READ

6.2 SSI 初始化(src/backend/storage/lmgr/procarray.c

if(XactIsoLevel==XACT_REPEATABLE_READ||XactIsoLevel==XACT_SERIALIZABLE){SISetup();// 初始化 SSI 结构}

6.3 可见性判断(src/backend/access/heap/heapam_visibility.c

无论哪种隔离级别,最终都调用HeapTupleSatisfiesMVCC(),但传入的快照不同。


七、常见陷阱与问题解决

7.1 陷阱 1:误以为 RR 能避免所有并发问题

虽然 RR 禁止不可重复读和幻读,但仍可能发生写偏斜(Write Skew)

-- 假设库存表:product_id, stock-- 业务规则:总库存 >= 0-- 事务 A (RR)SELECTstockFROMinventoryWHEREproduct_id=1;-- 5SELECTstockFROMinventoryWHEREproduct_id=2;-- 5-- 事务 B (RR)SELECTstockFROMinventoryWHEREproduct_id=1;-- 5SELECTstockFROMinventoryWHEREproduct_id=2;-- 5-- A: UPDATE inventory SET stock = 0 WHERE product_id = 1;-- B: UPDATE inventory SET stock = 0 WHERE product_id = 2;-- 结果:总库存 = 0,看似合法-- 但如果业务要求“任一产品库存不能低于 3”,则违反规则!

✅ 解决方案:使用SERIALIZABLE隔离级别,SSI 会检测到此冲突并回滚其中一个事务。

7.2 陷阱 2:长事务导致膨胀

RR 事务持有快照时间越长,阻止 VACUUM 清理的死元组越多,表膨胀风险越高。

建议:RR 事务应尽量短;设置idle_in_transaction_session_timeout

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

<span class=“js_title_inner“>以APB为例,多外设验证的陷阱</span>

做过SoC验证的工程师都知道&#xff0c;APB总线上通常挂着十几个甚至几十个外设。但很多团队验证DUT时&#xff0c;只搭建一个主机对一个从设备的环境&#xff0c;这种验证方式有个致命盲区——完全忽略了地址译码的真实场景。APB是典型的共享总线架构。PADDR、PWRITE、PWDATA这…

作者头像 李华
网站建设 2026/6/10 3:54:28

Java高频面试题:MyISAM索引与InnoDB索引的区别?

大家好&#xff0c;我是锋哥。今天分享关于【Java高频面试题&#xff1a;MyISAM索引与InnoDB索引的区别&#xff1f;】面试题。希望对大家有帮助&#xff1b;Java高频面试题&#xff1a;MyISAM索引与InnoDB索引的区别&#xff1f;MyISAM 和 InnoDB 是 MySQL 中两种不同的存储引…

作者头像 李华
网站建设 2026/6/10 19:25:05

uniapp调试鸿蒙元服务闪退怎么解决?

本问答帖原创发布在华为开发者联盟社区 &#xff0c;欢迎开发者前往论坛提问交流。 问题描述&#xff1a; 同样的代码使用华为真机调试APP可以正常运行&#xff0c;重新打开一个uniapp VUE3的项目启动元服务&#xff0c;HBuilder X和dev都能正常编译运行&#xff0c;但真机上打…

作者头像 李华
网站建设 2026/6/10 19:32:59

Full Circle柠檬杯好用吗?健康便携水杯推荐

柠檬杯作为一种专门设计用于泡制柠檬水的便携水杯&#xff0c;近几年在注重健康生活的人群中流行起来。它的核心设计通常包含一个隔离柠檬果肉和种子的内胆或压榨装置&#xff0c;旨在让用户方便地制作出风味浓郁且不会喝到籽的柠檬水。我使用过多款此类产品&#xff0c;发现其…

作者头像 李华
网站建设 2026/6/10 18:21:38

2026扫码点单系统-亿坊-一套系统搞定门店经营管理全部所需!

对于餐饮、茶饮等线下门店而言&#xff0c;数字化早已不是“选择题”&#xff0c;而是关乎生存的“必答题”。然而&#xff0c;许多商家面临的困境是&#xff1a;系统碎片化——扫码点单、收银、会员管理、后厨打印、外卖接单分别使用不同软件&#xff0c;数据不通&#xff0c;…

作者头像 李华