触发器函数内禁止直接执行NOTIFY,因其违反PostgreSQL内核事务隔离限制;正确做法是通过AFTER INSERT触发器将通知事件写入专用队列表,再由外部进程异步消费。

触发器函数里不能直接调用 NOTIFY
PostgreSQL 的触发器函数(TRIGGER FUNCTION)运行在事务上下文中,而 NOTIFY 命令本身是异步的、不参与事务回滚——但它**不能在触发器函数内部直接执行**,否则会报错:ERROR: cannot issue NOTIFY in a trigger function。这不是权限问题,是 PostgreSQL 内核限制。
真正能用的方式是:触发器函数只做「标记」或「记录」,把通知逻辑交给外部监听机制或异步任务。常见做法有三种:
- 在触发器中插入一条记录到专用通知表(如
notification_queue),再由外部进程轮询或监听该表变化 - 使用
pg_notify()(注意不是NOTIFY)——但这个函数仅存在于某些扩展(如pgmq或自定义 C 函数),原生不支持 - 借助
pg_cron或应用层定时任务消费队列,避免实时性过强带来的耦合
用 AFTER INSERT 触发器写入通知队列表
这是最稳妥、兼容性最好的方案。核心思路是解耦:触发器只负责持久化通知意图,不负责发送。
示例:假设你有个 orders 表,希望每次插入新订单就“通知下游服务”:
CREATE TABLE notification_queue (
id SERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
delivered BOOLEAN DEFAULT FALSE
);
<p>CREATE OR REPLACE FUNCTION queue_order_notification()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO notification_queue (event_type, payload)
VALUES ('order_created', jsonb_build_object(
'order_id', NEW.id,
'user_id', NEW.user_id,
'total', NEW.total,
'created_at', NOW()
));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;</p><p>CREATE TRIGGER trig_order_notify
AFTER INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION queue_order_notification();
注意点:
-
AFTER INSERT是必须的——BEFORE时NEW.id可能还没生成(尤其用SERIAL) -
payload用JSONB存储,方便下游解析;避免在触发器里拼接字符串或调用外部 HTTP - 别忘了加索引:
CREATE INDEX idx_notif_pending ON notification_queue (delivered) WHERE delivered = false;
如何让应用监听并消费通知队列
PostgreSQL 自身不提供“监听表变更”的原生机制,但你可以用几种方式驱动消费:
- 应用启动后起一个常驻协程,定期查
SELECT * FROM notification_queue WHERE delivered = false LIMIT 100,处理完再UPDATE ... SET delivered = true - 用
LISTEN/NOTIFY配合触发器更新队列表:在notification_queue上建另一个触发器,对每条插入执行PERFORM pg_notify('notification_channel', row_to_json(NEW)::text);,这样应用只需LISTEN notification_channel就能收到轻量提醒,再按需查表 - 如果用 Python,
psycopg2支持异步通知监听;Go 可用pglogrepl或轮询;Node.js 推荐pg-pubsub包封装
关键区别:这里的 NOTIFY 是在普通函数(非触发器函数)里调用的,所以合法。而触发器函数内仍禁止它。
避免在触发器里做耗时操作或外部调用
哪怕绕过 NOTIFY 限制,也绝对不要在触发器中发起 HTTP 请求、写文件、连其他数据库——这些会严重拖慢事务、导致锁等待甚至连接池耗尽。
典型反例:
-- ❌ 千万别这么写!
CREATE OR REPLACE FUNCTION send_http_on_insert()
RETURNS TRIGGER AS $$
BEGIN
-- 这个函数根本不存在于原生 PostgreSQL,且即使有也会阻塞事务
PERFORM http_post('https://api.example.com/webhook', ...);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
真正该做的,只是把必要字段记下来,让更合适的层(比如应用服务、消息队列消费者)去完成后续动作。触发器的职责边界很窄:原子、轻量、可回滚。
复杂点在于队列表的清理策略和失败重试设计——比如某条通知发送失败,你是标记为 error 后人工介入,还是自动进死信队列?这已经超出触发器能力范围了。

















