提到数据库事务隔离,大部分开发者对读已提交、可重复读都不陌生,但PostgreSQL还提供了一种更为特殊的隔离级别——可序列化快照隔离(Serializable Snapshot Isolation,简称SSI)。它在9.1版本正式引入,是PostgreSQL引以为傲的特性之一,因为它没有采用传统数据库那种基于两阶段锁的可序列化实现,而是借助一种乐观并发控制思路,在保持快照隔离性能优势的同时,提供了真正意义上的可序列化语义。这篇文章就来详细拆解SSI的原理、配置和使用方式。

一、从快照隔离到可序列化:SSI要解决什么问题
PostgreSQL内部实现了四种事务隔离级别,分别是读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)、可序列化(SERIALIZABLE)以及历史遗留的读未提交(实际等同于读已提交)。其中可重复读级别在PostgreSQL中的实现方式是基于快照隔离(Snapshot Isolation)的,每个事务在开始时获取一个数据库一致性快照,事务内的读操作始终看到同一份数据视图,写操作则通过First-Committer-Wins规则处理并发冲突。
快照隔离看起来已经很完善了,但它存在一个经典缺陷——写偏斜异常(Write Skew)。举个典型例子:医院值班系统中有一条约束,任意时刻至少要有一名医生值班。两名医生同时在不同事务中查询当前值班人数,都发现除自己之外还有人值班,于是各自把自己撤下,结果两个事务都提交成功,值班约束被破坏。这类异常的本质在于,两个并发事务分别读取了对方写入的数据集的一部分,却没有直接的写写冲突,快照隔离的冲突检测机制捕捉不到这种情况。
可序列化隔离的定义是:并发执行的结果必须等价于某种串行执行顺序。传统的实现方式是两阶段锁(2PL),读操作也要加共享锁并持有到事务结束,这会导致读写之间大量阻塞,并发性能很差。SSI的核心贡献在于,它通过在快照隔离基础上增加额外的检测机制来识别出那些可能导致非可序列化执行的危险情况,只有真正可能出问题的并发事务组合才会被回滚,其余事务照常运行,这是一种典型的乐观并发控制。
二、SSI的核心机制:SIREAD锁与危险结构检测
SSI的实现依赖两个关键构件。第一个是SIREAD锁,它在事务执行查询时被隐式获取,与传统意义上的行锁不同,SIREAD锁不阻塞任何操作,只是一种标记,用来记录某个事务曾经读取过哪些数据。SIREAD锁的粒度从行到页面再到整个关系逐级升级,如果一条SELECT扫描了大量行,系统会自动将锁粗化到表级别以控制内存占用。可以通过pg_locks视图观察到mode为SIReadLock的锁记录。
第二个构件是冲突检测算法。SSI理论基于一个被广泛引用的结论:在快照隔离下,如果并发事务的依赖关系图中出现由两个危险结构构成的环,才会导致不可序列化的执行。具体来说,PostgreSQL会追踪事务之间的rw-antidependency边(即一个事务读过的数据被另一个事务写入)。当一个事务提交时,如果检测到自己同时处于两条连续的rw依赖边之中——也就是说存在一个事务T1读了某些数据,自己读了T1写过的(或其他相关的)数据,同时自己的写入又与后续事务的读取产生关联——就构成了危险结构,系统会主动中止其中一个事务,通常是提交较晚的那个。
回到前面医生值班的例子,在SSI下两个事务不会同时成功。第二个尝试提交的事务会收到错误提示:could not serialize access due to read/write dependencies among transactions,错误码是40001。应用程序捕获到这个错误后,可以选择重试整个事务。由于冲突检测是精确的,只有真正构成危险结构的组合才会被中止,误伤率远低于两阶段锁方案下的阻塞排队。
三、如何启用与配置SSI
启用可序列化隔离非常简单,只需要在事务开始时声明:
-- 方式一:开启事务时指定 BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 方式二:修改会话级默认隔离级别 SET default_transaction_isolation = 'serializable'; -- 查看当前隔离级别 SHOW transaction_isolation;
需要注意,默认配置下数据库并不跟踪普通SELECT语句的谓词读取,只有当会话真正使用SERIALIZABLE级别时,SIREAD锁才会生效。另外,可序列化事务之间需要通过pg_serial状态以及谓词锁来协调,这些信息存储在共享内存中,相关参数主要有几个。
max_pred_locks_per_transaction控制每个事务平均可持有的谓词锁数量,默认64,如果业务中存在大量长事务扫描大表的场景,适当调大它可以减少锁粗化带来的误中止。此外还有max_pred_locks_per_relation和max_pred_locks_per_page用于控制锁升级的阈值。当共享内存中谓词锁槽位耗尽时,PostgreSQL会自动将细粒度锁合并为粗粒度锁,极端情况下升级到表级,这会显著增加误中止的概率。
监控方面可以关注pg_locks视图中SIReadLock的数量,以及pg_stat_database中的相关统计信息。判断一个可序列化事务是否需要重试,标准做法是检查SQLSTATE,40001表示序列化失败,40P01表示死锁,两种情况都值得重试:
-- 应用层重试伪逻辑(以Python为例的核心SQL部分) -- 捕获错误码 40001 或 40P01 后整体重试事务 BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT count(*) FROM oncall_doctors WHERE on_duty = true; -- 若满足条件则执行业务操作 UPDATE oncall_doctors SET on_duty = false WHERE doctor_id = $1; COMMIT;
四、SSI的性能特征与适用场景
SSI的性能与两阶段锁相比有明显不同的表现特征。当事务冲突率低时,SSI几乎不产生额外开销,读操作不会被写操作阻塞,系统吞吐接近快照隔离的水平;而当冲突率高时,被中止的事务需要重试,CPU消耗在无效工作上,吞吐会明显下滑。官方文档给出的经验值是,如果超过一定比例(大约百分之一到百分之几)的并发可序列化事务需要因序列化失败而重试,就应该考虑优化事务设计,比如缩短事务持锁时间、减少事务读取的数据范围,或者干脆退回更低隔离级别配合显式锁。
SSI特别适合的场景包括:读写混合且冲突概率低的OLTP业务、需要严格数据一致性但又不希望读写互相阻塞的应用,例如金融账户约束检查、库存下限保护、唯一性业务规则校验等。这些约束如果写在应用层很难做到原子性,而用SSI则可以把约束校验直接放进事务里,让数据库保证串行等价性。
同时也要清楚SSI的局限。长事务会持有大量SIREAD锁,容易触发锁粗化,导致其他事务被误中止;跨库或使用外部系统(如缓存在PostgreSQL之外的应用内存)的数据一致性无法被SSI覆盖;另外,只读事务在SSI下也有代价,虽然PostgreSQL对只读可序列化事务做了延迟提交优化,但它们仍然需要参与冲突检测流程。如果业务场景中热点数据高度集中、冲突频繁,传统的显式行锁或者采纳可重复读加业务层校验可能比SSI更实际。
总结来说,SSI代表了PostgreSQL在并发控制上的独特设计哲学:不做全局阻塞,而是精确识别真正会破坏可序列化语义的事务组合。如果你的系统需要强一致性保证,同时读写冲突又不密集,SSI是非常值得采用的方案,配合完善的重试机制,可以在正确性和性能之间取得很好的平衡。
PostgreSQLSSI可序列化快照隔离修改时间:2026-09-10 01:18:39