PostgreSQL 事务与并发系列 · 第一期
从 ACID 到 MVCC:PostgreSQL 并发控制的核心思想
本系列将带你系统掌握 PostgreSQL 事务与并发的每一步。第一期从 ACID 原则谈起,揭开 MVCC 如何实现”读不阻塞写、写不阻塞读”。
一、引言
在关系型数据库的世界里,事务(Transaction)是数据一致性与并发控制的基石。PostgreSQL 之所以被公认为”业界最先进的开源关系型数据库”,其强大的事务处理能力和高并发支持功不可没。
理解 PostgreSQ L的事务与并发机制,不仅仅是掌握几条 SQL 语句。它直接决定了:
- 你写的应用在高并发下能否稳定运行
- 你的数据库能否扛住千万级 QPS 的冲击
- 你遇到死锁、性能瓶颈时能否快速定位并解决
本系列将从 ACID 原则讲起,逐步深入到 MVCC 实现原理、隔离级别的行为差异、锁机制的分类与死锁排查,最后涵盖快照隔离、长事务与膨胀治理,帮助你对 PostgreSQL 的事务与并发模块建立系统性的认知。
二、事务的 ACID 原则
什么是事务?
用数据库的术语来说,事务是由一个或多个 SQL 语句组成的工作单元。最经典的例子是银行转账——从账户 A 扣款和向账户 B 入账,这两个操作要么一起成功,要么一起失败,绝不允许出现”钱扣了,但对方没收到”这种中间状态。
事务所解决的四大并发现象
PGSQL 的事务机制正是为了解决以下四大潜在问题而产生的:脏读、不可重复读、幻读、序列化异常(后文将详细说明)。
ACID 四大特性
ACID 是事务的四大核心属性,也是 PostgreSQL 设计并发控制机制的根本原则:
| 特性 | 含义 | PostgreSQL 的实现技术 |
|---|---|---|
| 原子性(Atomicity) | 事务中的所有操作要么全部成功提交,要么全部失败回滚 | 事务管理 + 撤销日志 |
| 一致性(Consistency) | 事务执行前后,数据库始终保持一致的状态(约束、规则) | 主键、外键、检查约束等 |
| 隔离性(Isolation) | 并发事务之间互不干扰 | 多版本并发控制(MVCC) |
| 持久性(Durability) | 事务一旦提交,其修改就永久保存,即使系统故障也不丢失 | 预写式日志(WAL)+ 恢复子系统 |
三、原始的并发控制:锁
在 MVCC 诞生之前,大多数数据库系统采用基于锁的并发控制机制,其中最具代表性的是 S2PL(严格两阶段锁):
- 读操作需要获取共享锁
- 写操作需要获取排他锁
这也正是 PostgreSQL 经典的“读不阻塞写,写也不会阻塞读”设计初衷得以实现的基础。
四、MVCC:PostgreSQL 并发的灵魂
4.1 什么是 MVCC?
多版本并发控制(Multi-Version Concurrency Control, MVCC)是 PostgreSQL 实现高并发隔离性的核心技术。
核心理念是:当数据被修改时,数据库不直接覆盖原有数据,而是创建一个新的版本,保留历史版本。
这种设计带来最直观的效果就是:读者永远不会被写者阻塞,写者也永远不会被读者阻塞。
4.2 MVCC 的核心组件
PostgreSQL 中的每一条元组(tuple)都带有两个系统字段:
- xmin:创建这行版本的事务 ID
- xmax:删除/过期这行版本的事务 ID
当事务读取数据时,PostgreSQL 会根据当前事务的隔离级别和快照信息,来决定应该看到哪个版本的数据,实现在无需加锁的情况下保证事务隔离性和一致性。
五、事务隔离级别
SQL 标准定义了四种隔离级别,但 PostgreSQL 在内部只实现了三种不同的隔离级别——因为其”读未提交”的行为实际等同于”读已提交”。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | PostgreSQL 默认? |
|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | ❌ |
| 读已提交 | 不可能 | 可能 | 可能 | ✅ 默认 |
| 可重复读 | 不可能 | 不可能 | 可能 | 🟡 可设置 |
| 可串行化 | 不可能 | 不可能 | 不可能 | 🟡 最高级别 |
三种的不可重复出现性及其影响:PostgreSQL 都提供了完整的保障。
六、快照隔离
6.1 什么是快照?
快照是 PostgreSQL 实现 MVCC 的核心数据结构。当一个事务开始时,PostgreSQL 会为该事务生成一个快照,记录:
xmin:当前未完成的最小事务 IDxmax:下一个被分配的事务 IDxip_list:当前活跃事务列表
通过这个快照,PostgreSQL 能准确地判断每个数据版本对当前事务是否可见。
6.2 不同隔离级别下的快照行为
- 读已提交:事务中的每条语句执行前都会重新获取一次快照,这意味着同一个事务中的不同语句可能看到不同的最新数据。
- 可重复读:事务只在其第一条语句执行时获取一次快照,后续所有语句都复用这个快照,保证整个事务内看到的数据是一致的。
- 可串行化:基于可重复读的快照机制,但额外增加了对事务间依赖的检测,在冲突时主动终止事务以保持”真正的串行化”语义。
七、PostgreSQL 中的锁
MVCC 减少了大多数情况下的锁竞争,但在某些场景下,锁依然是不可或缺的。
7.1 标准锁的分类
PostgreSQL 的锁分为多个类型,常见的有:
| 锁模式 | 使用场景 | 冲突模式 |
|---|---|---|
| 表级锁 | 控制对整个表的并发访问 | ACCESS SHARE / ROW EXCLUSIVE / ACCESS EXCLUSIVE 等 |
| 行级锁 | 控制对特定行的并发修改 | FOR UPDATE / FOR SHARE |
7.2 建议锁
PostgreSQL 提供了一种应用层面的锁机制——建议锁。应用可以选择任意一个 64 位键值作为锁标识,然后申请锁、执行业务逻辑、释放锁,实现跨进程的分布式协调,非常适合防止重复处理(如计费调度、消息队列中的任务重复执行)。
7.3 FOR UPDATE 与 FOR UPDATE SKIP LOCKED
SELECT ... FOR UPDATE 是一种显示行锁,它会锁定被选中的行,直到当前事务结束,确保这些行不会被其他事务并发修改。在 PostgreSQL 9.5 引入的 SKIP LOCKED 修饰符,改变了这一默认行为:当被锁定的行正在被其他事务持有时,SELECT ... FOR UPDATE SKIP LOCKED 会直接跳过这些行,只返回当前可锁定的行,这是构建任务队列、工作池模式的最佳实践。
八、死锁
8.1 什么是死锁?
死锁(Deadlock) 是指两个或多个事务互相等待对方释放锁,从而形成循环依赖,导致谁都无法继续执行的情况。
8.2 如何诊断死锁
当死锁发生时,PostgreSQL 会将相关信息记录到日志中。你可以通过查询 pg_locks 视图并结合 pg_stat_activity,定位到阻塞链中的具体进程及其正在执行的查询。
8.3 如何避免死锁
最佳实践是:让所有事务以一致的顺序访问资源。例如,如果需要同时更新多张表,可以在应用层面约定:总是先锁表 A,再锁表 B。
如果死锁确实发生了,PostgreSQL 的死锁检测器会在等待一段时间后(由 deadlock_timeout 参数控制)自动终止其中一个事务,让另一个继续执行。
九、总结与下期预告
在本期中,我们探索了事务的 ACID 原则、MVCC 的核心思想、隔离级别的行为差异、快照的机制,以及锁和死锁的基础知识。这些是你理解和驾驭 PostgreSQL 并发的”第一块拼图”。
第二期预告:我们将深入 MVCC 的内部实现,详细解读可见性判断规则、事务快照的内存布局,并通过实践案例让你亲手感知”读不阻塞写,写不阻塞读”到底是怎么做到的。
- 读已提交 vs 可重复读:实际查询中的可见性差异
- 快照源码解析:GetSnapshotData 函数的作用
- 从实战案例看 MVCC:一个行版本如何在不同隔离级别下被不同事务”看见”或”看不见”
下一期,我们将一起”动手”操作 MVCC,真正把它还原为代码和数据。敬请期待!