MVCC 与并发控制
PostgreSQL 的并发控制核心是快照隔离:每个事务看到的是某个时间点的数据库快照,读写互不阻塞。官方表述是“读不阻塞写,写不阻塞读”,这是 PG 并发性能的根基,也和 MySQL 的 MVCC 有本质差异。
版本链:xmin/xmax
PG 没有 undo log,旧版本直接留在堆表里。每个元组头部带:
- xmin:插入这个版本的事务 ID
- xmax:删除或锁定这个版本的事务 ID
- ctid:指向下一个版本的指针(HOT 链)
UPDATE 插入新版本,旧版本标记 xmax 后留在页里;DELETE 只标记 xmax。可见性判断 = 版本链上的 xmin/xmax 与当前事务快照的比较,配合 CLOG(事务提交状态)。
快照
每个事务(或每条语句,取决于隔离级别)生成快照,快照记录当时活跃事务的边界。元组可见性规则:
- xmin 已提交且在快照之前 → 可见
- xmin 活跃(未提交)→ 不可见
- xmax 已提交且在快照之前 → 已删除,不可见
关键差异:MySQL 的旧版本在 undo log 里,靠回滚指针回溯;PG 的旧版本在数据页里,靠版本链和 VACUUM 管理。所以 PG 的 UPDATE 会产生死元组,表会膨胀,需要 Vacuum(见 Vacuum 篇)。
并发控制手段
| 机制 | 用途 |
|---|---|
| 快照隔离(MVCC) | 普通读不加锁,读写互不阻塞 |
| 行锁 | UPDATE/DELETE 锁行,通过元组 xmax 和 MultiXact 实现 |
| 表锁 | DDL、ACCESS EXCLUSIVE 等 |
| 咨询锁(advisory lock) | 应用层锁,可模拟分布式锁 |
行锁与 MySQL 的差异:PG 没有间隙锁和临键锁(锁的是元组不是索引范围),这决定了隔离级别的行为差异(见事务篇)。死锁检测靠等待图,报错 40001,事务自动回滚。
与 MySQL MVCC 的对比
| 维度 | PostgreSQL | MySQL InnoDB |
|---|---|---|
| 旧版本存放 | 堆表内版本链 | undo log |
| 清理机制 | VACUUM | purge 线程 |
| 可见性依据 | xmin/xmax + CLOG | 隐藏字段 + Read View |
| 读锁 | 不加锁 | 不加锁(快照读) |
| 行锁范围 | 只锁元组 | 记录锁 + 间隙锁 + 临键锁 |
面试追问
- PG 的 MVCC 和 MySQL 有什么区别? 旧版本一个在堆里一个在 undo log。PG 靠 VACUUM 清理死元组,MySQL 靠 purge 线程
- 读会阻塞写吗? 不会。普通读走快照,不获取任何锁,写者创建新版本。这是快照隔离的核心
- xmin/xmax 是什么? 插入和删除该版本的事务 ID。版本链加 CLOG 判断可见性
- 咨询锁是什么? 应用层显式获取的锁,不绑定数据行,可模拟分布式锁
- PG 为什么没有间隙锁? 锁基于元组而非索引范围,配合 SSI(可串行化快照隔离)处理并发冲突,机制与 MySQL 的临键锁不同