05 PostgreSQL 为什么并发能力强: 事务、MVCC、锁与等待

如果只学 SQL,不学事务和锁,对 PostgreSQL 的理解一定是不完整的。数据库真正难的地方,不是“如何查到一条数据”,而是“当很多人同时读写时,怎样既正确又不把系统拖死”。这就是事务、MVCC 和锁要解决的问题。

先记住一个核心事实

PostgreSQL 不是靠“让所有人排队”来保证正确,而是靠 MVCC 尽量让读写并发进行,再在必要时用锁保护关键一致性边界。

这句话的含义很重要:

  • 读通常不需要和写硬碰硬
  • 写和写之间仍然可能竞争
  • 历史版本不是白来的,需要 vacuum 清理
  • 长事务会拖住版本回收

很多线上问题,其实都能回到这四条上解释。

三个最需要理解的概念

1. 事务

事务的价值不是“能回滚”,而是把一组操作变成一个一致性的单位。只要事务边界乱,系统行为就会越来越不可预测。

2. MVCC

MVCC 可以粗略理解为“同一行在不同时间对不同事务呈现不同可见性”。这让读不必频繁阻塞写,但也意味着系统需要管理更多版本信息。

3. 锁

锁不是敌人。锁是数据库在必须串行化某些操作时采取的保护。真正危险的是:

  • 事务太长
  • 拿锁顺序混乱
  • 热点对象过于集中
  • 业务不知道自己正在持有什么锁

开发者最容易犯的并发错误

1. 事务里夹外部调用

比如:

  • 事务开始
  • 更新数据库
  • 调远程接口
  • 等待第三方响应
  • 再提交

这会直接把锁持有时间拉长,也会把所有等待链问题放大。

2. 不统一加锁顺序

两个事务如果都要更新 A 和 B,但顺序一个是 A 再 B,另一个是 B 再 A,死锁概率会明显上升。

3. 长事务把垃圾回收拖死

长事务不一定自己很忙,但它会让旧版本长期不能清理,进而带来表膨胀、autovacuum 压力和性能下降。

隔离级别怎么理解才够用

对大多数业务来说,先记住这几点就够了:

  • read committed 是默认值,适合绝大多数普通业务。
  • repeatable read 更强调事务内读取一致,但要理解它对并发观察结果的影响。
  • serializable 最强,但也最贵,应该在真正需要的时候使用。

隔离级别不是越高越好,而是越匹配业务越好。

一条实用的锁等待排查 SQL

select
  a.pid,
  a.usename,
  a.state,
  a.wait_event_type,
  a.wait_event,
  pg_blocking_pids(a.pid) as blocking_pids,
  a.query
from pg_stat_activity a
where a.datname = current_database()
order by a.xact_start nulls last;

这条 SQL 的价值不是“万能”,而是帮你第一时间判断:

  • 谁在等
  • 谁在堵
  • 堵的链条大概有多长

减少锁问题的五条硬规则

  • 事务尽量短
  • 不在事务中做人为等待
  • 对热点资源保持一致的访问顺序
  • 批量操作拆成可控批次
  • 队列类场景优先考虑 for update skip locked

真正要警惕的不是死锁本身

死锁其实还算“诚实”,因为数据库会报错。更难受的是那些不死锁但长期等待的事务,它们会慢慢吞掉吞吐、拖高连接数、占住资源,让系统看起来像“越来越卡但又说不清为什么”。

所以,学事务与锁的真正价值,不是为了背概念,而是为了建立一套判断能力:

  • 这个问题是执行慢,还是在等待
  • 等待是偶发,还是结构性设计问题
  • 应该改 SQL、改事务边界、改业务顺序,还是改架构

只要这套判断开始建立,你就已经跨过了从“会用数据库”到“懂数据库”的分界线。