为什么在捕获到异常后执行COMMIT时,PostgreSQL会悄悄回滚?在PolarDB for PostgreSQL上也观察到了这一现象
我在使用 PolarDB for PostgreSQL 开发一个Python应用程序(阿里云的PostgreSQL兼容数据库,基于PostgreSQL 16、采用共享存储架构)。我遇到了一个令人困惑的事务行为,并在PostgreSQL 16(由REL_16_STABLE构建)上也确认存在,因此这似乎是PostgreSQL的核心设计。
最小可复现示例:
import psycopg2
conn = psycopg2.connect("dbname=mydb")
conn.autocommit = False
cur = conn.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, name TEXT NOT NULL)")
conn.commit()
# --- Start a new transaction ---
cur.execute("INSERT INTO users VALUES (1, 'Alice')") # Step 1: OK
try:
cur.execute("INSERT INTO users VALUES (2, NULL)") # Step 2: NOT NULL violation
except psycopg2.IntegrityError as e:
print(f"Caught: {e}")
# I caught it — shouldn't the program continue normally?
cur.execute("INSERT INTO users VALUES (3, 'Charlie')") # Step 3: BOOM
conn.commit()
我期望:
我在Python的 try/except中捕获了步骤2 的异常。我期望步骤3 成功,commit() 能保存第1 行和第3 行。
实际发生了:
Caught: null value in column "name" of relation "users" violates not-null constraint
psycopg2.errors.InFailedSqlTransaction:
current transaction is aborted, commands ignored until end of transaction block
步骤3 失败。如果我把它再包裹在一个try/except中并调用conn.commit(),PostgreSQL会悄悄执行一个 ROLLBACK — 甚至第1 行 ('Alice') 也会丢失。在某些驱动中,commit() 调用不会返回错误,这让无声的数据丢失更容易被忽视。
我的问题:
- 为什么捕获Python的异常并不能“修复”事务? 在Python的 try/except中确实捕获了IntegrityError对象,但服务器端的事务仍然处于中止状态。这两者是完全独立的状态机吗?
- 为什么COMMIT会悄无声息地变成ROLLBACK? 我明确要求提交。PostgreSQL将其转换为回滚且不抛出错误。这感觉像是一种无声数据丢失的根源——这是设计使然吗?
- 处理这种情况的正确模式是什么? 我发现SAVEPOINT可以提供帮助:
``` cur.execute("INSERT INTO users VALUES (1, 'Alice')")
cur.execute("SAVEPOINT sp1") try: cur.execute("INSERT INTO users VALUES (2, NULL)") except psycopg2.IntegrityError: cur.execute("ROLLBACK TO SAVEPOINT sp1")
cur.execute("INSERT INTO users VALUES (3, 'Charlie')") conn.commit() # Result: Alice and Charlie are saved! ```
但这看起来有些冗长。有没有更简洁的方式?
补充背景 — PolarDB的行为:
我在PolarDB for PostgreSQL上运行这个,它通过共享存储架构扩展PostgreSQL(一个读写节点,多个只读副本)。起初我怀疑PolarDB的复制或共享存储是否会影响这一行为,但测试结果表明该行为与PostgreSQL完全一致。这让我确信这是一种PostgreSQL的核心设计选择,而非PolarDB特有。话虽如此,在PolarDB的 Oracle兼容模式(polar_comp_stmt_level_tx)中,有一个选项可以实现语句级回滚,能完全避免这个问题——但我想先理解标准的PostgreSQL行为。
环境:PolarDB for PostgreSQL(基于PG 16)、Python 3.11、psycopg2 2.9.9
解决方案
Matthias的回答 对第一个问题提供了详尽的解答,我无话可补。
如果事务因错误而中止,COMMIT 将悄无声息地回滚。基本上,它以唯一可能的方式结束事务。关于这个主题,曾在pgsql-hackers邮件列表有过一个有趣的讨论,可能如果 COMMIT 抛出错误并仍然回滚,会更“正确”,但最终没有采取任何行动...
对于你的第三个问题,数据库事务的“原子性”保证意味着事务中的所有语句要么全部失败,要么全部成功,从而维持数据的一致性。因此,通常的做法是在遇到数据库错误时回滚整个事务。你也可以在另一个事务中尝试一些替代方案。
在那些回滚整个事务会让批处理作业的所有工作都被撤销的情况下,你可以使用SAVEPOINT。但要克制使用。 如果使用过多的SAVEPOINT(实现为子事务),你的数据库性能将受到严重影响。PostgreSQL v18引入了一个参数 subtransaction_buffers 以在一定程度上缓解这个问题,但这仍然是一个问题。