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、改事务边界、改业务顺序,还是改架构
只要这套判断开始建立,你就已经跨过了从“会用数据库”到“懂数据库”的分界线。