2. PostgreSQL 事务基础:ACID 特性、事务隔离级别、显式事务与隐式事务
事务是数据库系统中保证数据一致性与完整性的核心机制。在异步任务编排场景中,多个子任务可能共享同一个数据库连接或事务上下文,理解 PostgreSQL 的事务模型是设计可靠回滚策略与性能优化的前提。本节将系统梳理事务的 ACID 特性、隔离级别以及显式/隐式事务的使用方式。
2.1 ACID 特性
ACID 是数据库事务正确执行的四个基本特性,PostgreSQL 通过多版本并发控制(MVCC)和预写式日志(WAL)完整实现了这些特性。
| 特性 | 含义 | PostgreSQL 实现机制 |
|---|---|---|
| 原子性(Atomicity) | 事务中的所有操作要么全部成功提交,要么全部回滚,不存在部分执行的状态。 | 通过 WAL 日志和事务提交日志(clog)实现。回滚时利用 WAL 中的 undo 记录恢复数据页。 |
| 一致性(Consistency) | 事务执行前后,数据库必须满足所有预定义的约束、触发器和业务规则。 | 依赖约束检查(主键、外键、唯一、检查约束)和触发器在事务提交前验证。 |
| 隔离性(Isolation) | 并发执行的事务之间互不干扰,每个事务都感觉不到其他事务的并发执行。 | 基于 MVCC 的快照隔离,每个事务看到的是数据的一个一致性快照。 |
| 持久性(Durability) | 一旦事务提交成功,其对数据库的修改就是永久的,即使系统崩溃也不会丢失。 | WAL 日志在事务提交时强制刷盘(fsync),确保日志先于数据页写入。 |
异步任务中的 ACID 陷阱: 在编排多个异步子任务时,如果每个子任务使用独立数据库连接,原子性将无法跨连接保证。常见做法是使用 Saga 模式或两阶段提交(2PC)来弥补,但会引入额外的协调开销。
2.2 事务隔离级别
SQL 标准定义了四种事务隔离级别,用于控制并发事务之间的可见性。PostgreSQL 默认使用 读已提交(Read Committed),同时完整支持所有标准级别。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | 可能(PG 实际不会发生) | 可能 | 可能 | 可能 |
| 读已提交(Read Committed) | 不可能 | 可能 | 可能 | 可能 |
| 可重复读(Repeatable Read) | 不可能 | 不可能 | 可能(PG 不会发生) | 可能 |
| 可序列化(Serializable) | 不可能 | 不可能 | 不可能 | 不可能 |
PostgreSQL 隔离级别特殊行为:
- 读未提交 在 PG 中实际行为等同于 读已提交,因为 MVCC 架构天然防止脏读。
- 可重复读 在 PG 中通过快照隔离实现,不会出现幻读(标准 SQL 允许幻读)。
- 可序列化 使用可序列化快照隔离(SSI)技术,检测读写冲突并强制回滚冲突事务。
2.3 显式事务与隐式事务
PostgreSQL 支持两种事务管理方式:隐式事务(自动提交)和显式事务(手动控制)。理解两者的区别对于异步任务编排中的事务边界控制至关重要。
2.3.1 隐式事务(自动提交)
默认情况下,PostgreSQL 的每个 SQL 语句都被当作一个独立的事务执行,语句执行完毕后自动提交。这种方式适用于简单的单语句操作。
-- 每条语句自动提交
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 如果第一条成功、第二条失败,第一条的修改已经提交,无法回滚
异步任务风险: 在异步编排中,如果多个子任务使用隐式事务,任何一个子任务失败都无法撤销其他子任务已提交的修改。这是导致数据不一致的常见原因。
2.3.2 显式事务(手动控制)
使用 BEGIN 或 BEGIN TRANSACTION 启动事务,通过 COMMIT 提交或 ROLLBACK 回滚。显式事务允许将多个操作组合成一个原子单元。
-- 显式事务:原子性保证
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 如果任何一步失败,可以 ROLLBACK
COMMIT; -- 或 ROLLBACK;
2.3.3 事务控制语句速查
| 语句 | 作用 | 异步编排场景说明 |
|---|---|---|
BEGIN |
启动一个显式事务块 | 每个异步子任务组应使用独立的 BEGIN/COMMIT 包裹 |
COMMIT |
提交当前事务,使修改永久生效 | 所有子任务成功后再提交,避免部分提交 |
ROLLBACK |
回滚当前事务,撤销所有修改 | 任一子任务失败时触发全局回滚 |
SAVEPOINT |
在事务中设置保存点,支持部分回滚 | 可用于子任务粒度的回滚,保留其他子任务进度 |
RELEASE SAVEPOINT |
释放保存点,使其不再可用 | 子任务成功后释放对应保存点 |
ROLLBACK TO SAVEPOINT |
回滚到指定保存点,保留保存点之后的修改 | 实现子任务级别的部分回滚 |
2.3.4 保存点示例:子任务级回滚
BEGIN;
-- 子任务 1
UPDATE task_status SET state = 'running' WHERE task_id = 1;
SAVEPOINT sp_task1;
-- 子任务 1 的具体操作
INSERT INTO task_log VALUES (1, 'start', now());
-- 假设子任务 1 失败
ROLLBACK TO SAVEPOINT sp_task1;
-- 子任务 1 的修改被撤销,但事务仍在进行
-- 子任务 2
UPDATE task_status SET state = 'completed' WHERE task_id = 2;
-- 子任务 2 成功
COMMIT; -- 只提交子任务 2 的修改
性能优化建议: 在异步任务编排中,尽量将多个子任务合并到同一个显式事务中,减少事务提交次数(每次 COMMIT 涉及 WAL 刷盘)。使用保存点实现细粒度回滚,避免整个事务重做。
2.4 事务与异步任务的交互模式
在异步任务编排中,事务管理通常遵循以下三种模式:
- 单连接单事务: 所有子任务共享同一个数据库连接和事务,适合短流程、低并发的场景。
- 多连接独立事务: 每个子任务使用独立连接和独立事务,适合高并发但需要补偿事务(Saga)的场景。
- 多连接共享事务(XA/2PC): 通过两阶段提交协调多个连接的事务,适合分布式强一致性场景,但性能开销较大。
| 模式 | 原子性保证 | 性能 | 复杂度 | 推荐场景 |
|---|---|---|---|---|
| 单连接单事务 | 强(ACID) | 高(减少刷盘) | 低 | 短流程、低并发、内部任务 |
| 多连接独立事务 | 弱(需补偿) | 中(连接池开销) | 中 | 长流程、高并发、微服务 |
| 多连接共享事务 | 强(2PC) | 低(协调开销) | 高 | 分布式强一致性、金融场景 |
实践建议: 对于大多数异步任务编排场景,优先采用 单连接单事务 模式,配合保存点实现子任务级回滚。当任务必须跨数据库实例时,再考虑 Saga 或 2PC。