SQL 如何排查死锁(Deadlock detected)的日志解读模板

10次阅读

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

SQL 如何排查死锁(Deadlock detected)的日志解读模板

怎么看 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 12345Process 6789操作系统级的 backend PID,可结合 pg_stat_activity 查当前会话
  • transaction 67890transaction 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_startxact_start 判断事务挂了多久
  • 对长期 idle in transaction 的会话,要检查应用层是否漏了 COMMITROLLBACK,这是死锁高频诱因

为什么加了索引还是死锁?关键看锁范围

很多人以为“只要 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 t1UPDATE t2,会话 B 反过来先 UPDATE t2UPDATE t1
  • 在每个 UPDATE 后加 SELECT pg_sleep(0.1); 延迟,放大竞争窗口
  • 修复后验证重点不是“还死不死”,而是看 pg_locks 中锁的持有顺序是否统一:所有事务都按 t1 → t2 → t3 的固定顺序加锁,就能彻底避免环路
  • 上线前务必检查 ORM 是否隐式启用了 FOR UPDATE 或自动拆分批量更新(如 Djangobulk_update 在某些版本会逐条执行)

真正难的不是识别死锁,而是确认所有业务路径下的锁顺序是否收敛——尤其当微服务间通过消息队列异步触发 DB 操作时,锁顺序很容易被拆散。

text=ZqhQzanResources