首页 / 知识库 / python服务端进阶 / 数据库锁详解

05 - MVCC:PostgreSQL 的并发杀手锏

前面几章反复提到「PostgreSQL 普通读不加锁」,靠的就是 MVCC(Multi-Version Concurrency Control,多版本并发控制)。这是 PostgreSQL 高并发能力的核心,理解它,你就理解了 PG 为什么这么强。

一句话先记住:MVCC 让「读」和「写」不再互相阻塞——写的时候造一个新版本,读的人还读旧版本,谁也不挡谁。


5.1 传统锁的痛点:读写互相挡

回顾第 03 章的规则:读写互斥。在纯锁数据库里:

有人在写 id=1 → 排他锁 → 你想读 id=1 → 只能等他写完
有人在读 id=1 → 共享锁 → 你想写 id=1 → 只能等他读完

结果:读多写多的系统,大家互相阻塞,并发上不去。

MVCC 的目标就是打破这个僵局:让读永远不用等写,写也不用等读。


5.2 MVCC 的核心思想:数据留多个版本

关键点:当你修改一行数据时,PostgreSQL 不是「原地改掉」,而是「造一个新版本」,旧版本还留着。

一行数据被改了三次,PG 里其实存了多个版本:

  version 1: balance=100  (事务10 创建, 事务20 时失效)
  version 2: balance=500  (事务20 创建, 事务30 时失效)
  version 3: balance=300  (事务30 创建, 还有效)

每个版本都记着:
  · 我是哪个事务创建的(xmin)
  · 我是哪个事务删除/失效的(xmax)

于是同一行数据,在同一时刻,不同的事务可能看到不同的版本。这就是「多版本」。


5.3 快照:每个事务看到的「世界的一个切片」

事务开始时(或每条语句开始时,取决于隔离级别),PostgreSQL 给它一个快照(Snapshot)——相当于「此刻数据库的一张照片」。事务只看得到这张照片里「对它可见」的版本。

时间线:

  T1 时刻:balance = 100
  事务A 开始,拍了张快照 → A 眼里 balance 永远是它快照那一刻的值
  
  T2 时刻:事务B 把 balance 改成 500 并提交(造了新版本)

  T3 时刻:事务A 再读 → 依然看到 100(它读自己的快照/旧版本,不受B影响)
                     → A 不需要等 B,B 也不需要等 A

这就是魔法所在:A 在读旧版本,B 在写新版本,两人各干各的,互不阻塞。这正是「读写不互相阻塞」的来源。


5.4 可见性规则(简化理解)

一个版本对当前事务「可见不可见」,大致规则是:

一个数据版本,对我可见的条件(简化版):
  ✅ 创建它的事务,在我拍快照之前就已经提交了
  ✅ 且 它还没有被「在我快照之前提交的事务」删除/更新掉

简单说:我只看得到「在我开始之前就已经确定存在」的版本,
       别人在我之后做的改动,我看不见(也就不受影响)。

不同隔离级别,拍快照的时机不同:

隔离级别快照时机效果
Read Committed(默认)每条语句开始时拍新快照能看到别人已提交的最新数据(所以会「不可重复读」)
Repeatable Read事务开始时拍一次,全程用它整个事务看到的数据一致(避免不可重复读、幻读)

这也解释了第 01 章的隔离级别是怎么实现的——靠控制「什么时候拍快照」


5.5 MVCC 也不是万能的:写写还是要锁

MVCC 解决的是「读 vs 写」不阻塞。但「写 vs 写」呢?两个事务同时改同一行,总不能各造各的版本互不干涉——那就冲突了。

读 vs 读  → MVCC,不阻塞 ✅
读 vs 写  → MVCC,不阻塞 ✅(读旧版本,写新版本)
写 vs 写  → 还是要加行锁!❌ 同一行,后来的写要等前面的写提交

所以:MVCC 消灭了读写之间的阻塞,但同一行的并发写,仍然靠行锁来排队。 第 04 章的悲观/乐观锁,处理的正是这种「写写冲突」。


5.6 MVCC 的代价:膨胀与 VACUUM

多版本不是免费的。旧版本会一直占着磁盘空间,越积越多:

不断 UPDATE 同一批行
  → 产生大量「已经没人需要的旧版本」(死元组 dead tuple)
  → 表变得越来越大(表膨胀 / table bloat)
  → 查询变慢

PostgreSQL 用 VACUUM(清理) 机制来回收这些旧版本:

  • autovacuum:后台自动定期清理,一般不用管
  • 高频更新的表,可能需要关注 VACUUM 是否及时,否则会膨胀

重要认知:MVCC 用「多存几个版本 + 定期清理」的代价,换来了「读写不阻塞」的巨大并发收益。这个交易,对读多写多的系统非常划算。


本章小结

你要记住要点
MVCC 目标让读和写不互相阻塞
核心思想改数据时造新版本,旧版本留着,多版本并存
快照每个事务看到「某一时刻的数据切片」
与隔离级别靠「何时拍快照」实现不同隔离级别
局限只解决读写阻塞;写写冲突仍靠行锁
代价旧版本堆积(表膨胀),靠 VACUUM 清理

下一章预告:行锁用多了,会遇到一个经典麻烦——两个事务互相等对方的锁,谁也动不了。这就是死锁。


上一章 ← 04 - 乐观锁与悲观锁 | 下一章 → 06 - 死锁:产生、排查与避免