文章目录
-
- [一、事务隔离级别概述: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))
- [6.1 快照获取逻辑(`src/backend/utils/time/snapmgr.c`)](#6.1 快照获取逻辑(
- 七、常见陷阱与问题解决
-
- [7.1 陷阱 1:误以为 RR 能避免所有并发问题](#7.1 陷阱 1:误以为 RR 能避免所有并发问题)
- [7.2 陷阱 2:长事务导致膨胀](#7.2 陷阱 2:长事务导致膨胀)
PostgreSQL 作为一款高度可靠的开源关系型数据库,其事务隔离机制是保障数据一致性和并发性能的核心支柱。在 SQL 标准定义的四种隔离级别中,读已提交(Read Committed) 和 可重复读(Repeatable Read) 是最常被使用的两种。尽管它们名称相似,但在 PostgreSQL 中的底层实现机制却存在根本性差异------这种差异直接影响了应用的行为、性能表现和一致性保证。
本文将深入 PostgreSQL 内核,从 快照(Snapshot)获取时机、可见性判断逻辑、冲突检测机制 等维度,全面剖析 READ COMMITTED 与 REPEATABLE READ 的实现原理,并揭示 PostgreSQL 如何通过 MVCC + SSI(Serializable Snapshot Isolation) 技术,在不牺牲性能的前提下提供强一致性保障。
一、事务隔离级别概述:SQL 标准 vs PostgreSQL 实现
PostgreSQL 的 READ COMMITTED 与 REPEATABLE 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 READ 和 SERIALIZABLE 都基于 SSI 算法 实现,区别仅在于 是否启用冲突检测:
| 隔离级别 | 快照类型 | 是否记录读写依赖 | 是否检测冲突 | 冲突时行为 |
|---|---|---|---|---|
| READ COMMITTED | 语句级 | 否 | 否 | --- |
| REPEATABLE READ | 事务级 | 是 | 否 | 允许提交 |
| SERIALIZABLE | 事务级 | 是 | 是 | 回滚事务 |
SSI 的核心思想:
-
记录"危险结构"(Dangerous Structures):
- 事务 A 读取某行
- 事务 B 修改该行并提交
- 事务 A 后续又写入相关数据
→ 形成"读-写-写"依赖链,可能导致非串行化结果
-
构建序列化图(Serialization Graph)
-
检测环(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