postgresql触发器本身不直接制造死锁,但会隐式引入跨表dml操作,导致不可见的锁链和循环等待;排查需重点分析死锁日志中非业务主表(如audit_log、inventory)的sql对及等待关系,优化应避免强同步逻辑下推,改用应用层统一事务或异步队列。

PostgreSQL 中的 SQL 触发器本身不直接“制造”死锁,但会把原本单点、可预测的锁行为,变成隐式、多跳、跨表的锁链——而你从应用层完全看不到它。一旦触发器内含 DML(尤其是 UPDATE、INSERT 或 SELECT FOR UPDATE),就极易在高并发下形成循环等待。
触发器让锁路径不可见,但锁粒度没变小
你在应用里只发了一条 INSERT INTO orders,但 PostgreSQL 实际执行的是:INSERT + 触发器里的 UPDATE inventory + 可能还有 INSERT INTO audit_log。这三步共享同一个事务 ID、同一套锁生命周期,且按顺序加锁。问题在于:你无法从 pg_stat_activity 的 query 字段里看到后两步,它们藏在执行引擎内部。
- 应用日志只记录 “INSERT orders 成功”,但实际已悄悄锁住
inventory行和audit_log表 -
pg_locks会显示多个relation,但没有字段标明哪把锁来自触发器 - 死锁图(
SHOW ENGINE INNODB STATUS是 MySQL 的;PostgreSQL 需看日志)中出现多张表 + 多个pid交错等待,基本就是触发器搅局
BEFORE 触发器里用 NEW.status = 'done' 不等于安全
很多人以为 SET NEW.status = 'done' 在 BEFORE INSERT 里不走 SQL 就不会锁,这是对的——但它只解决“本行赋值”。一旦你要同步更新其他表(比如扣库存、记积分、发通知),就必须走真实 DML,这就立刻回到锁风险区。
-
BEFORE中改NEW字段:无额外锁,推荐用于单行字段修正 -
AFTER中UPDATE inventory SET qty = qty - 1 WHERE sku = NEW.sku:新增一次行锁,且可能因缺失索引升级为间隙锁 - 更危险的是
AFTER INSERT ... SELECT FOR UPDATE:显式加锁 + 触发器延迟执行 = 锁持有时间拉长 + 等待窗口扩大
PostgreSQL 死锁日志里怎么识别触发器痕迹?
死锁发生后,立刻查 postgresql.log,重点不是报错行,而是 DETAIL 块里两个事务的 SQL 和等待关系。触发器参与的典型信号有:
- 两条 SQL 看似无关(如一条是
INSERT orders,另一条是UPDATE audit_log),却在等同一行锁 - 等待链中出现非业务主表,比如
log_history、counter_cache、notification_queue—— 这些大概率是触发器写的 -
Process 12345 waits for ShareLock on transaction 98765; blocked by process 67890,而67890的 query 是INSERT,但它的锁却落在了inventory上 → 触发器已介入
真正该砍掉的不是触发器,而是“必须由触发器完成”的假设
很多团队用触发器是为了“保证一致性”,但一致性 ≠ 必须数据库侧强同步。PostgreSQL 的事务原子性只保障本事务内,不担保跨事务可见性。强行把统计更新、日志写入、通知分发塞进触发器,等于把应用层的调度逻辑下推到存储层,反而放大不确定性。
- 订单创建后要扣库存?用应用层事务包住
INSERT orders+UPDATE inventory,确保锁顺序统一 - 要记操作日志?用
INSERT INTO audit_log (...) VALUES (...)显式写,别靠触发器隐式追加 - 真需要异步副作用(如发消息)?触发器里只写轻量
INSERT INTO task_queue(BLACKHOLE 引擎或普通表),再由 worker 消费
触发器死锁最棘手的地方,是它让锁行为脱离开发者的控制视野——你调的是一条语句,数据库跑的是一个锁组合拳。排查时别只盯错误码 deadlock detected,得进日志翻 DETAIL 里的 SQL 对,看有没有“不该出现在这里的表”;优化时不靠调 deadlock_timeout,而要问一句:这件事,真的非得在 INSERT 的瞬间做完吗?











