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

文章目录

    • [一、事务隔离级别概述:SQL 标准 vs PostgreSQL 实现](#一、事务隔离级别概述:SQL 标准 vs PostgreSQL 实现)
      • [1.1 SQL 标准定义的隔离级别问题](#1.1 SQL 标准定义的隔离级别问题)
      • [1.2 关键差异对比表](#1.2 关键差异对比表)
      • [1.3 实践建议](#1.3 实践建议)
    • [二、核心机制基础:MVCC 与事务快照(Snapshot)](#二、核心机制基础:MVCC 与事务快照(Snapshot))
      • [2.1 快照(Snapshot)是什么?](#2.1 快照(Snapshot)是什么?)
      • [2.2 可见性判断](#2.2 可见性判断)
    • [三、读已提交(READ COMMITTED):语句级快照](#三、读已提交(READ COMMITTED):语句级快照)
      • [3.1 快照获取时机](#3.1 快照获取时机)
      • [3.2 行为示例](#3.2 行为示例)
      • [3.3 底层实现细节](#3.3 底层实现细节)
      • [3.4 优点与代价](#3.4 优点与代价)
    • [四、可重复读(REPEATABLE READ):事务级快照 + SSI 冲突检测](#四、可重复读(REPEATABLE READ):事务级快照 + SSI 冲突检测)
      • [4.1 快照获取时机](#4.1 快照获取时机)
      • [4.2 行为示例](#4.2 行为示例)
      • [4.3 底层实现:SSI(Serializable Snapshot Isolation)](#4.3 底层实现:SSI(Serializable Snapshot Isolation))
      • [4.4 为什么 RR 能禁止幻读?](#4.4 为什么 RR 能禁止幻读?)
    • 五、监控与诊断
      • [5.1 查看当前事务隔离级别](#5.1 查看当前事务隔离级别)
      • [5.2 检测长 RR/SERIALIZABLE 事务](#5.2 检测长 RR/SERIALIZABLE 事务)
      • [5.3 监控 SSI 冲突(仅 SERIALIZABLE)](#5.3 监控 SSI 冲突(仅 SERIALIZABLE))
    • [六、底层代码路径简析(PostgreSQL 15+)](#六、底层代码路径简析(PostgreSQL 15+))
      • [6.1 快照获取逻辑(`src/backend/utils/time/snapmgr.c`)](#6.1 快照获取逻辑(src/backend/utils/time/snapmgr.c))
      • [6.2 SSI 初始化(`src/backend/storage/lmgr/procarray.c`)](#6.2 SSI 初始化(src/backend/storage/lmgr/procarray.c))
      • [6.3 可见性判断(`src/backend/access/heap/heapam_visibility.c`)](#6.3 可见性判断(src/backend/access/heap/heapam_visibility.c))
    • 七、常见陷阱与问题解决
      • [7.1 陷阱 1:误以为 RR 能避免所有并发问题](#7.1 陷阱 1:误以为 RR 能避免所有并发问题)
      • [7.2 陷阱 2:长事务导致膨胀](#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 COMMITTED REPEATABLE READ
快照粒度 每条 SQL 语句 整个事务
不可重复读 允许 禁止
幻读 允许 禁止(PostgreSQL 特性)
SSI 依赖跟踪
冲突检测 否(但记录依赖)
性能开销 极低 略高(需维护依赖图)
适用场景 Web 应用、日志系统 金融交易、报表统计
首次访问触发 每次语句 事务中第一次读/写

1.3 实践建议

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

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

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

2.1 快照(Snapshot)是什么?

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

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

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

2.2 可见性判断

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


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

3.1 快照获取时机

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

3.2 行为示例

sql 复制代码
-- 会话 A
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 返回 100

-- 此时会话 B 执行:
-- UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT;

-- 会话 A 继续:
SELECT balance FROM accounts WHERE id = 1;  -- 返回 200!
COMMIT;

✅ 第二次查询看到了会话 B 已提交的修改

⚠️ 这就是"不可重复读"------被 SQL 标准允许,但在某些业务场景中是危险的

3.3 底层实现细节

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

3.4 优点与代价

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

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

4.1 快照获取时机

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

4.2 行为示例

sql 复制代码
-- 会话 A
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;  -- 返回 100

-- 会话 B:
-- UPDATE accounts SET balance = 200 WHERE id = 1; COMMIT;

-- 会话 A 继续:
SELECT balance FROM accounts WHERE id = 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)依赖

例如:

sql 复制代码
-- 事务 A (RR)
SELECT * FROM orders WHERE status = 'pending'; -- 返回 0 行

-- 事务 B
INSERT INTO orders (status) VALUES ('pending'); COMMIT;

-- 事务 A
UPDATE orders SET priority = 1 WHERE status = 'pending'; -- 影响 0 行

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


五、监控与诊断

5.1 查看当前事务隔离级别

sql 复制代码
SHOW transaction_isolation;
-- 或
SELECT current_setting('transaction_isolation');

5.2 检测长 RR/SERIALIZABLE 事务

sql 复制代码
SELECT pid, 
       query, 
       xact_start,
       now() - xact_start AS xact_age,
       wait_event_type,
       wait_event
FROM pg_stat_activity
WHERE state <> 'idle'
  AND (current_query LIKE '%REPEATABLE READ%' 
       OR current_query LIKE '%SERIALIZABLE%')
ORDER BY xact_age DESC;

5.3 监控 SSI 冲突(仅 SERIALIZABLE)

sql 复制代码
SELECT * FROM pg_stat_database_conflicts
WHERE datname = current_database();
-- 查看 serialization_failures

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

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

c 复制代码
Snapshot
GetTransactionSnapshot(void)
{
    if (IsolationUsesXactSnapshot())
        return GetSnapshotData(&CurrentSnapshotData);
    else
        return GetLatestSnapshot(); // READ COMMITTED 走这里
}
  • IsolationUsesXactSnapshot() 返回 true 当且仅当隔离级别 ≥ REPEATABLE READ

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

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)

sql 复制代码
-- 假设库存表:product_id, stock
-- 业务规则:总库存 >= 0

-- 事务 A (RR)
SELECT stock FROM inventory WHERE product_id = 1; -- 5
SELECT stock FROM inventory WHERE product_id = 2; -- 5

-- 事务 B (RR)
SELECT stock FROM inventory WHERE product_id = 1; -- 5
SELECT stock FROM inventory WHERE product_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

相关推荐
crossaspeed2 小时前
MySQL的MVCC
数据库·mysql
2401_857683542 小时前
为你的Python脚本添加图形界面(GUI)
jvm·数据库·python
m0_706653232 小时前
使用Python自动收发邮件
jvm·数据库·python
松涛和鸣2 小时前
DAY67 IMX6 Development Board Configuration from Scratch
数据库·postgresql·sqlserver
路由侠内网穿透.2 小时前
fnOS 飞牛云 NAS 本地部署私人影视库 MoonTV 并实现外部访问
运维·服务器·网络·数据库·网络协议
怣502 小时前
MySQL表筛选分组全解析:排序、分组与限制的艺术
数据库·mysql
tsyjjOvO2 小时前
JDBC(Java Database Connectivity)
java·数据库
陌上丨2 小时前
如何保证Redis缓存和数据库数据的一致性?
数据库·redis·缓存
l1t2 小时前
一个用postgresql的自定义函数求解数独的程序
数据库·postgresql·数独