bigywater.tech
常见报错 · 40

PostgreSQL ERROR: deadlock detected:先开始等锁的会话被回滚,服务端日志才有两边的语句,统一加锁顺序后死锁从 19/20 降到 0

“ERROR: deadlock detected” 在 PostgreSQL 里很常见,错误信息里只有进程号和事务号。这篇在本机自己起一个 PostgreSQL 16,复现反序更新引起的死锁,看谁被回滚、检测要等多久、服务端日志里多出什么信息,以及统一加锁顺序到底换来了什么。

1.53 s
默认 deadlock_timeout = 1s 时,死锁发生到两个会话都结束的总耗时(其中 0.5 s 是语句里的 pg_sleep)
19 / 20
一半按 1→2、一半按 2→1 的 20 个并发事务里,报 deadlock detected 的个数;统一顺序后是 0
40P01
死锁的 SQLSTATE,应用靠它识别并重试

报错长什么样

实测 · 本机执行输出
  会话 A(退出码 1),成功的 UPDATE 1 条:
      ERROR:  deadlock detected
      DETAIL:  Process <pid> waits for ShareLock on transaction <xid>; blocked by process <pid>.
      Process <pid> waits for ShareLock on transaction <xid>; blocked by process <pid>.
      HINT:  See server log for query details.
      CONTEXT:  while updating tuple (0,2) in relation "accounts"
  会话 B(退出码 0),成功的 UPDATE 2 条:
  两个会话一共用了 1.53 s;最后的余额:1|120 ; 2|80

DETAIL 里是两个进程互相等对方的事务;CONTEXT 告诉你是在更新哪个表的哪个元组。

谁被回滚

实测 · 本机执行输出
  6 次的被回滚会话和总耗时(s):[('A', 1.53), ('A', 1.53), ('A', 1.52), ('A', 1.53), ('A', 1.52), ('A', 1.52)]
  把启动顺序换过来(B 比 A 早 0.05 s 开始):被回滚的是 ['B']
  被回滚的会话 A 整个事务撤销,B 的两条 UPDATE 都生效。最后余额:1|120 ; 2|80(A 的两条 UPDATE 都没有生效)
  事务里出错之后,同一个事务里后续语句会怎样(举 1/0 为例,死锁之后同理):
      ROLLBACK
      ERROR:  division by zero
      ERROR:  current transaction is aborted, commands ignored until end of transaction block
  COMMIT 之后 id=1 的余额:100(整个事务回滚了,UPDATE 没有生效)

先等满 deadlock_timeout 的会话来检查并中止自己;被回滚的事务整个撤销。

检测耗时

实测 · 本机执行输出
  deadlock_timeout = 100ms :两个会话总共用了 0.63 s,被回滚的是 ['A']
  deadlock_timeout = 1s    :两个会话总共用了 1.53 s,被回滚的是 ['A']
  deadlock_timeout = 3s    :两个会话总共用了 3.53 s,被回滚的是 ['A']

等待时间约等于 deadlock_timeout。

怎么排查

实测 · 本机执行输出
      pg.log: [<pid>] LOG:  process <pid> detected deadlock while waiting for ShareLock on transaction <xid> after 1000.730 ms
      pg.log: [<pid>] DETAIL:  Process holding the lock: <pid>. Wait queue: .
      pg.log: [<pid>] CONTEXT:  while updating tuple (0,64) in relation "accounts"
      pg.log: [<pid>] STATEMENT:  update accounts set bal = bal - 10 where id = 1; select pg_sleep(0.5); update accounts set bal = bal + 10 where id = 2;
      pg.log: [<pid>] ERROR:  deadlock detected
      pg.log: [<pid>] DETAIL:  Process <pid> waits for ShareLock on transaction <xid>; blocked by process <pid>.
      pg.log: 	Process <pid> waits for ShareLock on transaction <xid>; blocked by process <pid>.
      pg.log: 	Process <pid>: update accounts set bal = bal - 10 where id = 1; select pg_sleep(0.5); update accounts set bal = bal + 10 where id = 2;
      pg.log: 	Process <pid>: update accounts set bal = bal - 20 where id = 2; select pg_sleep(0.5); update accounts set bal = bal + 20 where id = 1;
      pg.log: [<pid>] HINT:  See server log for query details.
      pg.log: [<pid>] CONTEXT:  while updating tuple (0,64) in relation "accounts"
      pg.log: [<pid>] STATEMENT:  update accounts set bal = bal - 10 where id = 1; select pg_sleep(0.5); update accounts set bal = bal + 10 where id = 2;

服务端日志里有两个会话各自的语句,客户端看不到。

怎么避免

实测 · 本机执行输出
  A 按 1→2、B 按 2→1,20 个事务里 19 个报 deadlock detected(用时 2.3 s)
  全部按 1→2,20 个事务里 0 个报 deadlock detected(用时 6.1 s)
  应用靠 SQLSTATE 识别并重试死锁(VERBOSITY=verbose 时 psql 显示它):
      ERROR:  40P01: deadlock detected

统一加锁顺序,代价是并发事务排队;应用要能按 40P01 重试。

分步演示

死锁
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64;报错文本都是真实复现时原样捕获的(命令行工具统一用 LC_ALL=C 取英文原文,路径换成 );所有服务都是本机上自己起的、只监听 127.0.0.1,不访问外网;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用 PostgreSQL 16.15(Homebrew);自己起的实例(自己的数据目录,端口 9261,只监听 127.0.0.1,fsync 关闭,实验结束后整个目录删除),不碰本机已有的 PostgreSQL;“会话”是 psql 进程,每个用一条带 pg_sleep 的多语句 -c 命令构造(同一条命令里的多条语句在同一个事务里);元组位置和进程号每次运行都不同
    • 没有验证:外键和唯一索引引起的死锁(这里只测了两行 UPDATE 反序);SELECT … FOR UPDATE / FOR SHARE 的死锁;死锁检测对 pg_sleep 以外的真实应用负载的开销;max_locks_per_transaction;advisory lock 死锁;应用驱动(psycopg、JDBC 等)里异常类的名字与自动重试;PostgreSQL 其他版本的错误文字;连接池里被中止的事务如何复位
    京ICP备2026067502号-1京公网安备11010502063487号