引言
当后端应用同时为众多用户提供服务时,数据库就成为一致性博弈的核心战场。两位用户争抢最后一个座位、支付确认与退款同时发生、报表读取数据时更新正在进行——这些场景无一不触及 PostgreSQL 的事务与并发机制。一旦处理失当,就会导致静默的数据损坏、重复下单或余额异常。
本课程聚焦于后端开发者必须掌握的并发行为推理能力:PostgreSQL 如何将操作封装在事务中、隔离级别如何改变并发事务之间的可见性、MVCC 如何让读写互不阻塞,以及行级锁实际在何处生效。本课程的目标并非深入数据库管理,而是构建生产环境中遇到「两个请求执行了相同操作」或「仪表盘显示了陈旧数据」等 bug 报告时所需的实用心智模型。
ACID 与事务边界
PostgreSQL 中的事务是一个要么完全提交、要么完全回滚的工作单元。四项保障——原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)——描述了当你使用 BEGIN 和 COMMIT 包裹语句时数据库所做出的承诺。
在后端开发中,典型的形式如下:
如果第二个 UPDATE 执行失败,第一个也会被回滚。绝不会出现钱已扣减但未入账的状态。这里的原子性不是锦上添花——而是账目平衡的根本保障。
后端开发者需要注意的一个实际问题:除非显式执行 BEGIN,否则 PostgreSQL 将每一条单独的语句视为其自身的隐式事务。因此,如果在没有事务块的情况下执行两条独立的 UPDATE 语句,每条语句都会独立提交。这正是许多「部分生效」bug 的根源——这些 bug 通常出现在通过连接池或使用默认自动提交设置的 ORM 与数据库交互的应用代码中。
PostgreSQL 中的隔离级别
SQL 定义了四种隔离级别:READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ 和 SERIALIZABLE。PostgreSQL 实际实现了其中三种不同的行为 —— 由于 MVCC 的底层工作机制,READ UNCOMMITTED 的行为与 READ COMMITTED 相同。
你选择的级别决定了事务可以从并发事务中看到什么。READ COMMITTED 是默认值,通常也是你想要的:每条语句看到的是该语句开始之前已提交的数据。如果在两次 SELECT 之间有另一个事务提交了一次更新,第二次 SELECT 将看到新值 —— 这可能会让那些期望稳定视图的人感到意外。
REPEATABLE READ 将快照锁定为事务开始时刻,而非每条语句开始时刻。SERIALIZABLE 则更进一步:它会检测可串行化冲突,并中止其中一个冲突事务,返回一个序列化失败错误,应用程序必须准备好进行重试。

MVCC 与行级锁
PostgreSQL 使用多版本并发控制(MVCC)。当你 UPDATE 某一行时,数据库并不会原地覆盖旧版本,而是写入一个新的行版本,并将旧版本标记为已死。读操作看到的是其快照时刻已提交的版本。这就是为什么长时间运行的报表查询不会阻塞写入,反之亦然 —— 它们操作的是不同的行版本。
但 MVCC 并不能消除所有阻塞。当两个事务尝试 UPDATE 同一行时,PostgreSQL 需要确定一个赢家,输家会在行级锁上等待,直到第一个事务提交或回滚。SELECT FOR UPDATE 是在执行业务逻辑(依赖当前值)之前显式获取该锁的标准方式。
SELECT FOR UPDATE 后接条件 UPDATE 的模式,是解决经典竞态条件的标准方案:在该竞态中,两个请求各自读取值,都判断安全,然后都执行减扣操作。如果没有显式加锁,两个事务会读到相同的原始库存数量,从而导致超卖。
常见陷阱与实践要点
后端代码中经常出现三种失败模式。第一,长时间运行的事务会持有快照并阻止元组清理,导致表膨胀、性能随时间下降 —— 应及时关闭事务,不要在 HTTP 请求开始时打开事务,然后让它跨多个下游调用保持打开。第二,当两个事务以相反的顺序获取锁时就会发生死锁;应在整个代码库中保持一致的加锁顺序,或者依赖 PostgreSQL 检测并中止其中一个事务再进行重试。第三,在 SERIALIZABLE 下忽略序列化失败会导致数据损坏 —— 数据库是在告诉你这些操作实际上并不适合并行运行,重试逻辑是契约的一部分,而非可选项。
实践思路:将隔离级别和锁的选择视为应用设计的一部分,而非数据库调优细节。在关键代码段使用默认的 READ COMMITTED 并显式加 SELECT FOR UPDATE,可以干净地解决大多数并发 bug。当正确性比吞吐量更重要,并且你能处理失败时,可以考虑使用 SERIALIZABLE。
摘要
事务将多个语句捆绑成一个原子单元。隔离级别控制并发事务之间能看到彼此多少未提交的修改。MVCC 通过保留多个行版本,让 PostgreSQL 实现读写互不阻塞;而行级锁则在两个事务针对同一行时解决冲突。对后端开发者来说,具体的工具包括:BEGIN/COMMIT 的规范使用、用于防止竞态条件更新的 SELECT FOR UPDATE 模式,以及通过一致的加锁顺序避免死锁。请有意识地选择隔离级别——默认级别在大多数场景下足够,但如果默认级别的行为不够严格,REPEATABLE READ 和 SERIALIZABLE 可用于那些需要更强一致性的场景。
课程检查点
巩固所学知识,获取作业反馈。