文章目录
- 一、事务隔离级别概述: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 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),定义了当前事务“能看到哪些数据”。它包含三个关键字段:
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 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)依赖
例如:
-- 事务 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