06 - 死锁:产生、排查与避免
行锁带来了高并发,也带来一个经典麻烦:两个事务互相等着对方手里的锁,谁都不肯放,就一起卡死了。这就是死锁。这一章讲清楚它怎么产生、怎么排查、怎么避免。
一句话先记住:死锁 = A 等 B 的锁,B 又等 A 的锁,形成了一个「谁也不让谁」的环。
6.1 死锁是怎么产生的
来看一个最经典的例子——两个转账事务,加锁顺序相反:
事务A(给账户1、2 转账) 事务B(给账户2、1 转账)
│ │
│ 锁住 账户1 🔒 │
│ │ 锁住 账户2 🔒
│ 想锁 账户2 ... │
│ ⏳ 等待(被B占着) │ 想锁 账户1 ...
│ │ ⏳ 等待(被A占着)
│ │
└──── A 等 B,B 等 A,死锁!────┘
画成图就是一个「环」:
账户1 ──被A锁── A
↑ │
等待 等待
│ ↓
B ──被B锁── 账户2
A 在等 B 释放账户2,B 在等 A 释放账户1,形成闭环 → 死锁
6.2 死锁的四个必要条件
理论上,死锁的形成需要同时满足四个条件(了解即可,重点是最后一条最好破):
1. 互斥 —— 锁是独占的(这是锁的本性,没法改)
2. 持有并等待 —— 拿着一把锁,还去请求另一把
3. 不可剥夺 —— 别人的锁不能硬抢,只能等它自己放
4. 循环等待 —— A等B、B等A 形成环 ← ★ 最容易被破坏的一环
破解死锁的关键,通常是破坏第 4 条「循环等待」——只要大家按同样的顺序加锁,就不会成环。
6.3 PostgreSQL 怎么处理死锁
好消息:PostgreSQL 会自动检测死锁。它定期检查是否有「等待环」,一旦发现:
PostgreSQL 检测到死锁:
→ 选一个事务作为「牺牲品」,强制让它报错回滚
→ 另一个事务就能拿到锁,继续执行
你会看到类似的报错:
ERROR: deadlock detected
DETAIL: Process 123 waits for ShareLock on transaction 456;
Process 456 waits for ShareLock on transaction 123.
所以死锁在 PostgreSQL 里不会永久卡死,而是「其中一个事务被牺牲掉、报错回滚」。你的应用要能捕获这个错误并重试。
6.4 怎么排查死锁
1. 看应用日志 / 数据库日志里的 "deadlock detected" 报错
→ PostgreSQL 会告诉你是哪两个进程、在等什么锁
2. 查当前的锁等待情况(死锁高发时实时看):
-- 查看当前有哪些锁、谁在等谁(简化用法)
SELECT * FROM pg_locks WHERE NOT granted;
-- 查看当前活跃的、可能在等待的会话
SELECT pid, state, wait_event_type, query
FROM pg_stat_activity
WHERE state != 'idle';
3. 定位到冲突的两段业务代码,看它们的加锁顺序是不是反的
6.5 怎么避免死锁(重点)
死锁重在预防。几条实用建议:
① 统一加锁顺序(最重要)
让所有事务按同一个顺序访问/锁定资源。比如「永远先锁 id 小的账户,再锁 id 大的」:
改造前(会死锁): 改造后(不会死锁):
A: 锁1 → 锁2 A: 锁1 → 锁2
B: 锁2 → 锁1 B: 锁1 → 锁2 ← 都按 id 升序锁
→ 大家排队,不会成环
这是破坏「循环等待」,是最根本的办法。
② 缩短事务、尽早提交
事务越短,持锁时间越短,撞车概率越低。别在事务里做耗时操作(比如事务中间去调用外部 API、等用户输入)。
③ 一次锁定所需的所有行
如果能一条语句锁完(比如 SELECT ... WHERE id IN (1,2) FOR UPDATE ORDER BY id),就别分两次锁,减少「持有并等待」。
④ 降低锁的粒度和范围
只锁真正需要的行,别动不动锁大范围,减少冲突面。
⑤ 应用层做好重试
死锁无法 100% 避免。业务代码要能捕获 deadlock detected 错误,自动重试几次。
本章小结
| 你要记住 | 要点 |
|---|---|
| 死锁本质 | 互相等待对方的锁,形成环 |
| 四个条件 | 互斥、持有并等待、不可剥夺、循环等待 |
| PG 处理 | 自动检测,牺牲一个事务回滚报错 |
| 排查 | 看 deadlock 日志、查 pg_locks / pg_stat_activity |
| 避免 | 统一加锁顺序(最关键)、缩短事务、应用层重试 |
下一章预告:全部讲完了,最后用一张大表把所有锁——粒度、模式、乐观悲观、MVCC——串起来对比,方便你速查。
上一章 ← 05 - MVCC:PostgreSQL 的并发杀手锏 | 下一章 → 07 - 总结:一张表看懂所有锁