PostgreSQL Transactions 事务实用教程
什么是事务
事务(Transaction)把一组数据库操作作为一个整体执行:全部成功就提交,出现问题则回滚,避免只完成一半的业务操作。
BEGIN;
-- 一组 SQL 操作
COMMIT; -- 确认并保存
-- 或 ROLLBACK; -- 撤销本事务尚未提交的更改
-- 一组 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 请求和文件写入不是数据库事务的一部分;需设计补偿、可靠消息或事务发件箱等方案。
- **不要盲目重试所有错误:**只对明确可重试的并发失败进行有限重试,并从事务开始处重新执行。
- **提交前确认业务结果:**检查受影响行数、约束与关键返回值;确认成功后再提交。