30 PostgreSQL ACID特性详解

上一篇我们聊了缓存管理的双缓冲现实和命中率的判断逻辑。这一篇,我们进入第 6 章并发控制与事务管理,从最基础也最核心的概念开始:ACID。原子性、一致性、隔离性、持久性------这四个词几乎出现在每一本数据库教材里,但真正理解它们在 PostgreSQL 中"由哪个机制兑现、代价是什么、边界在哪里"的人并不多。这一篇,我们把 ACID 逐项拆开,对应到 PostgreSQL 的具体实现,最后回答两个问题:ACID 中哪个最难实现?PG 如何在一致性与性能之间做权衡?

一、ACID 不是四个并列的"功能",而是四个不同层面的承诺

先把四个词的含义对齐:

特性 承诺 一句话理解
原子性(Atomicity) 事务内的所有操作,要么全部生效,要么全部不生效 没有"做了一半"的状态
一致性(Consistency) 事务只能把数据库从一个合法状态带到另一个合法状态 约束不被违反
隔离性(Isolation) 并发事务之间互不干扰,每个事务看到一致的数据视图 并发执行的结果等价于某种串行顺序
持久性(Durability) 一旦事务提交成功,其结果就不会丢失 崩溃、断电也不怕

这四个承诺不是同一层面的东西。原子性和持久性是"实现机制"问题,隔离性是"并发控制"问题,一致性则更多是"应用语义"问题。 把它们混在一起讨论,就容易得出"PostgreSQL 的 ACID 是完美的"这种过于简单的结论。

二、原子性:WAL 和事务状态共同兑现

原子性的核心要求是:事务执行到一半崩溃,不能留下"部分生效"的数据。

PostgreSQL 的实现依赖两个机制:

第一,WAL 记录所有变更,但不代表变更已生效。 事务执行过程中,对数据页的修改会写入 WAL,但这些修改是否"算数",取决于事务的最终状态。如果事务回滚,WAL 中仍然有这些变更的记录,但恢复时它们不会被当作已提交的结果。

第二,pg_xact 记录事务的最终状态。 每个事务在 pg_xact 中占用 2 个比特位,标记为"已提交""已回滚"或"进行中"。原子性的"裁决权"在 pg_xact,不在 WAL。 崩溃恢复时,PostgreSQL 重放 WAL 把数据页恢复到崩溃前的物理状态,然后根据 pg_xact 中的事务状态,决定哪些变更对后续事务可见。

这解释了一个容易困惑的现象:回滚的事务,其修改可能已经写入了数据页。 因为 PostgreSQL 没有 UNDO,回滚不是"撤销已做的修改",而是"在 pg_xact 中标记该事务为已回滚,让这些修改对后续事务不可见"。空间回收由 VACUUM 完成。

原子性的代价: 每个事务都要在 pg_xact 中留下状态记录,pg_xact 不能无限增长;回滚不立即释放空间,依赖 VACUUM 清理。这就是为什么长事务和频繁回滚会加剧表膨胀。

三、一致性:约束是"硬保障",应用逻辑是"软保障"

一致性的定义比较微妙:事务不能违反数据库的完整性约束。 但"合法状态"的范围,很大程度上由应用定义,而不是由数据库定义。

PostgreSQL 提供的一致性保障机制包括:

  • NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK:在写入时强制检查,违反则事务失败;
  • EXCLUSION 约束:范围不重叠等业务规则,由数据库直接保证;
  • 触发器:可以在写入前后执行自定义逻辑,但触发器的正确性由开发者负责;
  • 延迟约束(DEFERRABLE) :允许约束检查推迟到事务提交时,适合"先插入子表再插入父表"这类场景。

关键认知:数据库只能保证"它知道的约束"不被违反。 如果业务规则是"账户余额不能为负",而你没有用 CHECK 或触发器把它表达出来,数据库不会替你把关。一致性是数据库机制和应用逻辑的共同责任,ACID 中的"C"从来不是数据库单方面能兑现的。

一致性的代价: 约束检查需要额外的 CPU 和索引维护;外键的级联操作可能引入锁竞争;延迟约束把检查推迟到提交时,可能让错误发现得更晚。

四、隔离性:MVCC 是主角,隔离级别是旋钮

隔离性是 ACID 中最复杂、也最能体现 PostgreSQL 设计选择的一项。

PostgreSQL 的隔离性由 MVCC 实现:每个事务根据自己的快照判断行版本的可见性,读不阻塞写、写不阻塞读。但"隔离到什么程度"由隔离级别决定:

隔离级别 PostgreSQL 中的行为 能防止的异常
Read Uncommitted 行为等同于 Read Committed 无额外保障
Read Committed(默认) 每条语句获取新快照 脏读
Repeatable Read 事务内使用同一快照 脏读、不可重复读、幻读(PG 的实现比 SQL 标准更强)
Serializable SSI(可串行化快照隔离) 所有并发异常,等价于串行执行

PostgreSQL 的 Read Committed 是语句级快照 :同一事务内,不同语句可能看到不同的数据。Repeatable Read 是事务级快照 :整个事务看到同一个时间点。Serializable 在 Repeatable Read 基础上增加了 SSI 冲突检测,如果检测到可能导致串行化异常的并发模式,会主动中止其中一个事务,要求应用重试。

隔离性的代价: 隔离级别越高,并发冲突导致事务中止的概率越大,应用需要处理重试逻辑;Serializable 的 SSI 检测需要额外跟踪读写依赖,有一定开销。隔离性和并发吞吐是一对矛盾------提高隔离级别,往往意味着更多的事务中止和重试。

五、持久性:WAL 刷盘是"最后一公里"

持久性的要求是:COMMIT 返回成功之后,即使立即断电,这个事务的结果也不能丢。

PostgreSQL 的兑现方式是:在 COMMIT 时,确保该事务的所有 WAL 记录已经被 fsync 到磁盘,然后才向客户端返回成功。 数据页本身可以还没刷盘------持久性由 WAL 保证,不由数据页保证。

这个"最后一公里"由 synchronous_commit 参数控制:

  • on(默认) :等待本地 WAL 刷盘后才返回;
  • remote_apply / on / remote_write:在流复制场景下,还可以要求备库确认;
  • off :提交立即返回,WAL 刷盘由后台异步完成,崩溃时可能丢失最近一小段时间内已提交的事务,但数据库本身仍然一致。

持久性的边界: 持久性只保证"已提交事务不丢失",不保证"未提交事务不丢失"(它们本来就不该保留),也不保证"存储介质本身不损坏"。如果磁盘控制器缓存没有掉电保护、或者文件系统在 fsync 上撒谎,WAL 刷盘也可能失效。PostgreSQL 能保证的是"在操作系统和硬件诚实的前提下,持久性成立"。

持久性的代价: 每次 COMMIT 的 fsync 是同步 I/O,延迟直接叠加到事务提交时间上。synchronous_commit = off 用"可能丢失最近若干毫秒的提交"换取更高的提交吞吐,这是一个明确的权衡。

六、ACID 中哪个最难实现?

如果只选一个,答案是 隔离性。

原因有三:

第一,隔离性需要在正确性和并发性之间做连续权衡。 原子性和持久性可以做到"默认全有"------WAL 刷盘、事务状态标记,都是确定性的机制。隔离性则是一个旋钮:Read Committed 并发高但异常多,Serializable 正确性强但冲突多。没有"全对且全快"的选项。

第二,隔离性的正确性证明极其复杂。 SSI 要检测的是"是否存在一个等价的串行执行顺序",这涉及读写依赖图的环检测。PostgreSQL 的 SSI 实现需要跟踪 SIREAD 锁、检测危险结构,并在必要时中止事务。这种机制的复杂度和调优难度,远超"刷 WAL"或"标记事务状态"。

第三,隔离性的故障模式最隐蔽。 原子性出问题会直接报错,持久性出问题会丢数据,都很容易被发现。隔离性出问题表现为"偶发的数据不一致"------两个事务并发执行,结果不符合任何串行顺序,可能几个月才出现一次,排查极其困难。

一致性的难度则取决于应用。 如果约束设计得完整,数据库能兜住大部分;如果业务规则只存在于代码里,再强的数据库也保证不了。

七、PG 如何权衡一致性与性能?

PostgreSQL 的权衡策略可以概括为:默认选择"足够强"的保证,把"更强"和"更快"都做成显式选项。

维度 默认选择 放宽的选项 收紧的选项
隔离性 Read Committed Read Uncommitted(等同 RC) Repeatable Read、Serializable
持久性 synchronous_commit = on off、local remote_apply(复制确认)
一致性 约束立即检查 DEFERRABLE 延迟到提交 更严格的 CHECK、EXCLUDE、触发器
原子性 完整 WAL + pg_xact 无(不建议放宽) ---

这个设计的哲学是:默认配置面向"大多数业务能接受的一致性",而不是"理论最强"或"理论最快"。 需要更高一致性时,显式开启 Serializable 或 remote_apply,并接受性能代价;需要更高吞吐时,显式关闭 synchronous_commit,并接受数据丢失窗口。

一个常见的误判是:把"默认 Read Committed"当成"弱隔离"。 实际上,Read Committed 已经防止了脏读,对绝大多数 OLTP 业务足够。真正需要 Serializable 的场景(如金融转账的复杂多表校验),才值得付出冲突重试的代价。

八、总结

特性 PostgreSQL 的实现 关键代价
原子性 WAL 记录变更 + pg_xact 裁决事务状态;回滚不撤销物理修改 回滚空间依赖 VACUUM 回收
一致性 约束(NOT NULL/UNIQUE/FK/CHECK/EXCLUDE)+ 触发器 + 延迟约束 约束检查的 CPU 和索引开销;应用逻辑仍需负责业务规则
隔离性 MVCC + 快照 + 隔离级别(RC/RR/Serializable SSI) 高隔离级别导致事务中止和重试
持久性 COMMIT 时 WAL fsync 到磁盘 fsync 同步 I/O 延迟;off 用丢失窗口换吞吐

两句话回答核心问题:

ACID 中最难实现的是隔离性。 它不是"有或没有"的开关,而是一个需要在正确性、并发性和实现复杂度之间持续权衡的连续谱;它的故障模式也最隐蔽,往往以"偶发的不一致"形式出现,排查成本最高。

PG 的权衡策略是"默认够强,强弱可选"。 默认 Read Committed + synchronous_commit = on,覆盖大多数业务;需要更强一致性时显式开启 Serializable 或复制确认,需要更高吞吐时显式放宽持久性。ACID 不是数据库单方面赐予的,而是数据库机制和应用选择共同兑现的。

下一篇,我们进入事务隔离级别与案例:Read Committed 和 Repeatable Read 在 PostgreSQL 中到底差在哪里?为什么 PG 的 Repeatable Read 能避免幻读?Serializable 的 SSI 又是如何检测冲突的?


思考题:

你的业务目前用的是哪个隔离级别?有没有因为默认 Read Committed 遇到过"同一事务内两次查询结果不一致"的情况?另外,你有没有把 synchronous_commit 设成 off 来换吞吐------如果设了,你能接受多大的数据丢失窗口?欢迎在评论区聊聊。

相关推荐
syagain_zsx1 小时前
ODB 与 MySQL:C++ 里的 ORM 实践(类即表、事务铁律与查询封装)
开发语言·数据库·c++·mysql·工具库·1024程序员节
时间售卖铺1 小时前
MySQL 备份
运维·数据库·mysql·云原生
靠谱的工程师朋友2 小时前
电子会计档案管理软件科普 核心能力与选型要点解析
网络·数据库·人工智能·电子会计档案
北冥you鱼2 小时前
Go 语言 Ticker 定时任务全解析:原理、陷阱与最佳实践
开发语言·数据库·golang
花间相见2 小时前
【计算基础|MySQL17】三大日志:undo / redo / binlog
数据库·后端·面试
严同学正在努力2 小时前
PostgreSQL 数据库安装部署教程(Linux 多发行版 + Windows + Docker 全覆盖)
linux·数据库·postgresql
众链网络2 小时前
年卡跨淡旺季的有效期模型:不可用日期、次数与顺延规则
java·前端·数据库
shunwahma2 小时前
MySQL 替代选型,为什么首选 OceanBase 集中式?
数据库·mysql·oceanbase
n 55!w !1083 小时前
MySQL数据库笔记整理1
数据库·笔记·mysql