跳到主要内容

PostgreSQL Transactions 事务实用教程

什么是事务​

事务(Transaction)把一组数据库操作作为一个整体执行:全部成功就提交,出现问题则回滚,避免只完成一半的业务操作。

BEGIN;
-- 一组 SQL 操作
COMMIT; -- 确认并保存
-- 或 ROLLBACK; -- 撤销本事务尚未提交的更改

PostgreSQL 默认使用自动提交:每条独立 SQL 成功后都会立即提交。要让多条语句属于同一个事务,需要显式开启事务,或由应用程序在同一个数据库连接中管理事务。

ACID 四个特性​

特性含义示例
原子性(Atomicity)事务中的操作全部生效,或全部撤销。转账不能只扣款、不入账。
一致性(Consistency)事务执行前后都应满足业务规则和数据库约束。余额不能违反设定的检查约束。
隔离性(Isolation)并发事务按隔离级别互相隔离,避免读写产生不符合预期的结果。并发修改同一账户时需要正确协调。
持久性(Durability)提交成功后,数据在数据库故障恢复后仍应保留。已提交的订单不会因服务重启而消失。

转账示例:把相关操作放进同一个事务​

假设账户表为 accounts(id, balance)。扣款和入账必须作为一个整体执行;扣款条件同时检查余额,避免余额不足时继续转账。

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100 RETURNING id, balance;
-- 应用程序必须检查上一步是否恰好更新一行。
-- 若没有更新一行(例如余额不足或账户不存在),执行
ROLLBACK,不要入账。 UPDATE accounts SET balance = balance + 100 WHERE id = 2 RETURNING id, balance;
-- 确认两步结果都符合预期后提交;否则回滚。
COMMIT;
注意

重要:示例中的行数检查需要由客户端或存储过程完成。若第一条更新未命中,必须停止后续操作并回滚;只有在两边账户及金额均验证通过后才能提交。实际系统还应处理重复请求、账户不存在和并发规则。

SAVEPOINT:事务中的局部回滚​

保存点可以撤销事务的一部分操作,而保留保存点之前的操作。

BEGIN;
UPDATE products SET stock = stock - 1 WHERE id = 10 AND stock > 0;
SAVEPOINT before_optional_log;
INSERT INTO order_logs(order_id, message) VALUES (5001, '订单已创建');
-- 如果可选日志写入失败并希望继续此事务,可回滚到保存点
ROLLBACK TO SAVEPOINT before_optional_log;
-- 完成其他必要操作并确认无误后再提交
COMMIT;

ROLLBACK TO SAVEPOINT 不会结束整个事务;RELEASE SAVEPOINT 可以释放不再需要的保存点。

隔离级别与并发现象​

隔离级别决定事务可以观察到哪些并发变化。PostgreSQL 默认隔离级别是 READ COMMITTED。

隔离级别PostgreSQL 中的行为使用提示
READ UNCOMMITTED按 READ COMMITTED 执行,不会读取未提交数据。设置后不会获得比 READ COMMITTED 更弱的行为。
READ COMMITTED每条语句开始时获取新的快照;同一事务的两次查询可能看到不同的已提交结果。默认级别,适用于许多常规业务。
REPEATABLE READ事务内使用一致的快照;并发更新冲突可能导致事务失败。适合需要事务内稳定读取的场景;失败时应重试整个事务。
SERIALIZABLE提供最严格的可串行化保证;数据库可能因检测到冲突而中止事务。应用必须识别序列化失败,并在安全条件下重试整个事务。
BEGIN ISOLATION LEVEL REPEATABLE READ;
-- 当前事务中的查询与更新
COMMIT;
-- 也可以在事务开始后、执行查询前设置
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 当前事务中的查询与更新
COMMIT;
信息

PostgreSQL 的 REPEATABLE READ 采用快照隔离,不会出现幻读,但仍可能遇到需要更高隔离级别处理的并发业务异常。SERIALIZABLE 事务被中止时,通常需要从事务开头重新执行,而不是只重试失败的那条语句。

锁与并发更新​

更新或删除行时,PostgreSQL 会对相关数据加锁。需要先读取再决定如何更新时,可以使用 SELECT ... FOR UPDATE 锁定目标行,直到事务结束。

BEGIN;
SELECT id, balance FROM accounts WHERE id = 1 FOR UPDATE;
-- 在锁仍持有时校验业务规则并执行更新
UPDATE accounts SET balance = balance - 50 WHERE id = 1 AND balance >= 50;
COMMIT;
注意

并发事务可能发生死锁。例如两个事务以相反顺序锁定相同的多行。数据库会中止其中一个事务;应用应处理该错误并在安全时重试。尽量让所有业务按一致顺序访问资源,并缩短事务持锁时间。

常见错误与安全实践​

  • **事务中某条语句报错:**PostgreSQL 通常会将事务标记为失败。应执行 ROLLBACK,或回滚到错误前建立的保存点;不能直接继续普通 SQL 操作。
  • **不要长时间保持事务打开:**长事务会持有锁,并可能妨碍清理旧版本数据。尽快完成并提交或回滚。
  • 确保同一连接:BEGIN、事务内 SQL 与 COMMIT/ROLLBACK 必须在同一个数据库连接中执行。
  • **外部操作不随数据库回滚:**邮件、HTTP 请求和文件写入不是数据库事务的一部分;需设计补偿、可靠消息或事务发件箱等方案。
  • **不要盲目重试所有错误:**只对明确可重试的并发失败进行有限重试,并从事务开始处重新执行。
  • **提交前确认业务结果:**检查受影响行数、约束与关键返回值;确认成功后再提交。