死锁是加锁顺序缺陷
PostgreSQL 不阻止死锁,而是检测循环并中止其中一个事务。错误是症状:两个事务以相反顺序获取锁,或持锁时间过长形成循环。服务器日志包含涉及的语句和进程,应从这里开始,而不是从应用堆栈猜起。
ERROR: deadlock detected
DETAIL: Process 120 waits for ShareLock on transaction 121;
blocked by process 121.
让顺序确定
如果转账更新账户 1 和 2,另一笔更新 2 和 1,按 id 排序后再加锁可以消除循环:
BEGIN;
SELECT id, balance FROM accounts
WHERE id IN (1, 2)
ORDER BY id
FOR UPDATE;
UPDATE accounts SET balance = balance - 10 WHERE id = 1;
UPDATE accounts SET balance = balance + 10 WHERE id = 2;
COMMIT;
ORDER BY id ... FOR UPDATE 以稳定顺序获取行锁。批处理任务按主键顺序处理行,并小批量提交。不要在事务打开期间调用支付服务或等待人工,否则毫秒级锁会变成数秒的冲突窗口。
有界重试
死锁是暂时的,有界重试是合适的。捕获 SQLSTATE 40P01,回滚,带抖动等待后重试整个事务。限制尝试次数,失败时暴露错误而不是无限循环。确保事务没有会在重试时重复的外部副作用;如果有,移到 outbox 或使其幂等。
关注趋势
跟踪每分钟死锁数和涉及的主要语句。突然增加通常跟随新的查询计划、缺失索引或某条代码路径改变了加锁顺序。pg_stat_database.deadlocks 提供计数,服务器日志提供细节。修顺序,不要只修超时。