PostgreSQL死锁日志需重点定位互锁的进程PID、事务ID、锁对象及具体SQL;结合pg_stat_activity查活跃会话与query,确认未提交事务;索引不能避免所有死锁,锁范围受隔离级别、执行计划影响;修复关键在于统一所有业务路径的加锁顺序。

怎么看 PostgreSQL 的 deadlock detected 错误日志
PostgreSQL 在发生死锁时会主动中止其中一个事务,并在日志里输出带 deadlock detected 的错误行。关键不是“有没有报错”,而是要从日志里快速定位哪两个(或多个)事务、哪几条 SQL、锁在了哪些对象上。
典型日志片段长这样:
ERROR: deadlock detected DETAIL: Process 12345 waits for ShareLock on transaction 67890; blocked by process 6789. Process 6789 waits for ShareLock on transaction 12345; blocked by process 12345. HINT: See server log for query details. CONTEXT: while updating tuple (123,45) in relation "orders"
-
Process 12345和Process 6789是操作系统级的 backend PID,可结合pg_stat_activity查当前会话 -
transaction 67890和transaction 12345是内部事务 ID,不能直接查,但能说明循环等待发生在两个事务之间 -
ShareLock on transaction ...表示是事务级锁冲突(常见于 SERIALIZABLE 隔离级别),不是行锁或表锁;若看到RowExclusiveLock on relation "xxx",则是普通 DML 引发的行级死锁 -
while updating tuple (123,45) in relation "orders"是最实用的线索:说明卡在更新orders表第 123 页第 45 行——结合表结构和索引,基本能反推出被锁的主键或唯一键值
如何关联到具体 SQL 和会话状态
单看错误日志不够,必须立刻查 pg_stat_activity 抓快照,尤其是死锁发生前后 10 秒内的活跃会话。重点过滤 state = 'active' 或 state = 'idle in transaction' 的记录,并按 backend_pid 匹配日志里的进程号。
- 用
SELECT pid, usename, application_name, client_addr, backend_start, state_change, query FROM pg_stat_activity WHERE pid IN (12345, 6789);查对应会话正在执行的query - 注意
state_change时间戳是否接近死锁日志时间——若差几秒内,大概率就是它 - 如果
query显示为或截断严重,说明语句已执行完但事务未提交,此时要看backend_start和xact_start判断事务挂了多久 - 对长期 idle in transaction 的会话,要检查应用层是否漏了
COMMIT或ROLLBACK,这是死锁高频诱因
为什么加了索引还是死锁?关键看锁范围
很多人以为“只要 WHERE 条件走索引,就只锁匹配行”,但 PostgreSQL 的锁行为受隔离级别、语句类型、执行计划共同影响。例如:
-
UPDATE orders SET status = 'shipped' WHERE user_id = 123;—— 若user_id没有索引,会全表扫描并锁住所有扫描过的行(甚至页),大幅增加死锁概率 - 即使
user_id有索引,若优化器选了 Bitmap Heap Scan + Recheck,也可能临时锁住非目标行 -
SELECT ... FOR UPDATE在可重复读下会锁住所有扫描到的可见行(包括已删除但未清理的 dead tuple),而不仅限于最终返回结果 - 批量操作如
UPDATE ... WHERE id IN (1,2,3,4,5),若这些id在物理存储上不连续,锁顺序可能和应用执行顺序不一致,形成环路
怎么复现和验证修复效果
死锁无法靠日志“修好”,必须在测试环境模拟相同访问模式。不要依赖随机压测,要精准控制事务顺序:
- 用两个 psql 会话,手动执行两段 SQL,严格按生产中出现的交叉顺序:比如会话 A 先
UPDATE t1再UPDATE t2,会话 B 反过来先UPDATE t2再UPDATE t1 - 在每个
UPDATE后加SELECT pg_sleep(0.1);延迟,放大竞争窗口 - 修复后验证重点不是“还死不死”,而是看
pg_locks中锁的持有顺序是否统一:所有事务都按t1 → t2 → t3的固定顺序加锁,就能彻底避免环路 - 上线前务必检查 ORM 是否隐式启用了
FOR UPDATE或自动拆分批量更新(如 Django 的bulk_update在某些版本会逐条执行)
真正难的不是识别死锁,而是确认所有业务路径下的锁顺序是否收敛——尤其当微服务间通过消息队列异步触发 DB 操作时,锁顺序很容易被拆散。










