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

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 - 总结:一张表看懂所有锁