PostgreSQL在事务隔离级别的选择上与MySQL走了不同的路线:MySQL的InnoDB引擎默认使用可重复读(Repeatable Read),而PostgreSQL则默认采用读已提交(Read Committed)。这个看似不起眼的差异,却会直接影响应用程序在并发场景下的行为表现。理解读已提交级别的语义和实现原理,是写好并发安全代码的基础,也是排查脏读、丢失更新等问题的前置知识。

一、读已提交级别的具体语义
读已提交是SQL标准中定义的四个隔离级别之一,它的核心承诺只有两条:一个事务只能看到其他已经提交的事务所做的修改,且不会出现脏读。换句话说,事务中的每一条语句,看到的都是该语句开始那一刻已经提交的最新数据快照。
这里有一个非常关键的细节容易被人误解:读已提交针对的是语句级别,而不是事务级别。PostgreSQL官方文档明确指出,在RC级别下,同一个事务内的每一条SQL语句都会获取一个新的快照。这意味着如果你在同一个事务里先执行一次SELECT,中间隔了几秒再执行同样的SELECT,两次查询可能返回不同的结果,这种现象称为不可重复读。此外,如果同一个查询在执行过程中扫描到被其他事务修改的行,还可能出现幻读。
来做一个简单的实验。打开两个psql会话,会话A执行下面的事务:
-- 会话A BEGIN; SELECT balance FROM account WHERE id = 1; -- 返回 1000 -- 此时去会话B执行提交,再回来查询 SELECT balance FROM account WHERE id = 1; -- 返回 2000,结果变了 COMMIT;
会话B中执行的语句如下:
-- 会话B UPDATE account SET balance = 2000 WHERE id = 1;
只要会话B的UPDATE在会话A两次SELECT之间提交完成,会话A的第二次查询就会看到新值2000。这就是读已提交的典型表现:事务可以看到外部世界在语句间隙发生的变化。这与可重复读完全不同,后者会在整个事务期间固定使用第一条查询的快照,两次SELECT结果始终一致。
二、MVCC如何支撑读已提交的实现
PostgreSQL没有使用传统的锁来实现读一致性,而是依赖MVCC(多版本并发控制)。每一行数据在物理上都会携带几个隐藏的系统字段,其中最重要的是xmin和xmax。xmin记录创建或最后更新该行版本的事务ID,xmax记录删除或更新该行版本的事务ID(更新在PostgreSQL中被实现为删除加插入的组合)。判断一行对当前查询是否可见的核心逻辑,就是比较xmin和xmax对应事务的状态:只有事务已经提交,它产生的行版本才可见。
在读已提交级别下,快照的获取时机是每条语句开始时。PostgreSQL会调用内部的快照获取函数,记录下当前的活跃事务列表,随后这 条语句扫描数据时,根据快照判断每个行版本的可见性。可见的旧版本直接返回,不可见的版本则沿着多版本链回溯查找。由于读操作完全不阻塞写操作,写操作也不阻塞读操作,RC级别能提供非常高的并发吞吐。
可以用下面的查询直接观察行的版本信息:
SELECT xmin, xmax, balance FROM account WHERE id = 1; -- xmin 是最后修改该行的事务ID, xmax 非零说明有删除或更新操作发生过
需要说明的是,PostgreSQL的可重复读是通过SSI(可串行化快照隔离)的前身,快照隔离技术实现的,事务开始后的第一条查询决定整个事务的快照点。因此从RC切换到可重复读,本质上是改变了快照的获取策略,从每语句一次变成每事务一次,成本变化不大,但语义差异巨大。而Oracle或MySQL中常见的锁定读(SELECT FOR UPDATE的等待行为)在PostgreSQL中也有独特表现,这一点下面会展开。
三、UPDATE遇到并发修改时的阻塞与重试
读已提交级别下最值得深入理解的行为,是UPDATE语句在目标行被其他未提交事务锁定时的处理方式。假设事务A和事务B同时执行UPDATE account SET balance = balance - 100 WHERE id = 1,PostgreSQL的规则是:后到的UPDATE会阻塞等待,直到先前的持有者事务结束。
这里的行为分为两种情况。如果持有锁的事务回滚了,等待的UPDATE正常获取锁并继续执行,逻辑上没有任何问题。但如果持有锁的事务提交了,等待的UPDATE不会直接在旧版本数据上操作,而是会重新评估该行的最新版本是否仍然满足WHERE条件,即EvalPlanQual机制。如果新版本仍满足条件,UPDATE会在新版本上继续执行;如果新版本已经不满足条件,这行会被跳过,不会报错。这种行为有效避免了基于过期数据做计算的问题。
-- 会话A BEGIN; UPDATE account SET balance = balance - 500 WHERE id = 1; -- 锁定该行,未提交 -- 会话B UPDATE account SET balance = balance - 300 WHERE id = 1; -- 阻塞,等待会话A的结果 -- 若会话A提交,B会在新版本上重新评估条件并执行扣减
尽管EvalPlanQual机制保证了单条UPDATE语句的正确性,但它不能解决应用层面的丢失更新问题。典型错误写法是先SELECT读出余额,在应用代码中计算新值,再用UPDATE写回,整个过程如果依赖RC级别,两次读到的数据可能不一致,最终覆盖掉其他事务的修改。正确的做法有三种:使用原子UPDATE语句(如上面的balance = balance - 100)、使用SELECT FOR UPDATE显式加锁,或者改用可重复读并在冲突时重试事务。
四、隔离级别选型建议与实践配置
PostgreSQL共支持三个隔离级别:读已提交、可重复读和串行化,标准中的读未提交在PostgreSQL中被当作读已提交处理。查看和设置当前级别的方式如下:
SHOW default_transaction_isolation; -- 查看默认级别 SET default_transaction_isolation = 'repeatable read'; -- 会话级修改 BEGIN ISOLATION LEVEL REPEATABLE READ; -- 单事务指定 BEGIN ISOLATION LEVEL SERIALIZABLE; COMMIT;
选型上可以遵循这样的思路:绝大多数OLTP业务,读已提交配合原子更新语句和显式行锁就足够了,它锁冲突少、吞吐量高,是PostgreSQL将其设为默认的合理之处。报表类或需要一致性快照导出的任务,适合用可重复读,让整个事务看到同一时刻的数据视图。对正确性要求极高、并发冲突不激烈的场景(比如金融账户间的资金转移),串行化级别通过SSI检测危险的读写依赖并主动中止部分事务,能从数据库层面保证可串行化执行,代价是冲突时应用必须实现重试逻辑。
最后提醒一点,切换隔离级别不是银弹。串行化事务一旦被中止会抛出序列化失败错误,应用代码必须捕获SQLSTATE为40001的错误并重新执行整个事务,否则比RC级别下的隐式风险更糟。总体而言,读已提交作为默认级别在性能与安全之间取得了很好的平衡,理解它的语句级快照语义和UPDATE阻塞重试机制,比盲目调高隔离级别更有实际价值。
PostgreSQL读已提交隔离级别修改时间:2026-09-09 21:38:50