NOTIFY仅发送会话级消息,不触发异步任务;真正异步需外部监听进程收到后调用dblink_send_query执行,或直接在存储过程中用dblink_send_query发起。

Listen/Notify 不能在存储过程里直接触发异步任务
LISTEN 和 NOTIFY 本身是会话级同步机制,不是任务调度器。你在 PL/pgSQL 函数里写 NOTIFY channel_name, 'payload',只是向监听该 channel 的客户端发一条消息——它不会自动启动新连接、不会执行 SQL、更不会“后台跑任务”。很多开发者误以为 NOTIFY 能替代 dblink_send_query 或外部队列,结果发现任务根本没执行。
常见错误现象:
- 存储过程里调用
NOTIFY后,查询立刻返回,但预期的后续逻辑(比如更新状态表、发邮件)完全没发生 - 用
pg_notify()(如果存在自定义封装)也一样,它只是包装了NOTIFY,不改变语义
真正能触发动作的,是外部应用监听到消息后主动发起的新请求。所以“异步任务”实际发生在数据库之外。
想让 Notify 驱动后台任务,必须配一个常驻监听进程
这本质是“数据库事件驱动 + 外部 worker”的模式。PostgreSQL 不提供内置 worker,你需要自己搭或用现成工具:
- 用 Python +
asyncpg写个监听服务:连接数据库,调用conn.add_listener('channel_name', callback),收到通知后用新连接执行目标 SQL - 用 Node.js +
pg库,配合pg-listen包,收到NOTIFY后触发client.query() - 用
pg_cron做兜底轮询(不推荐,延迟高且浪费资源)
关键点:
- 监听进程必须保持长连接,不能每次收到通知就重连(否则可能丢消息)
-
NOTIFY不保证送达,如果监听进程断连期间发了通知,消息就丢了(PostgreSQL 不持久化 notify 队列) - payload 最大 8000 字节,超长内容得存到表里,只在 notify 中传 ID
示例片段(Python asyncpg):
async def on_notify(conn, pid, channel, payload):
# 这里启动新连接执行任务,避免阻塞监听
async with pool.acquire() as task_conn:
await task_conn.execute("UPDATE jobs SET status = 'running' WHERE id = $1", int(payload))
dblink_send_query 才是存储过程内真异步执行 SQL 的办法
如果你坚持要在函数里“发起一个不等结果的 SQL”,dblink_send_query 是唯一靠谱选择:
- 它通过另一个数据库连接(dblink)提交 SQL,当前函数立即返回
- 任务在服务端后台运行,和客户端断连无关
- 可配合
dblink_get_result后续轮询状态(或忽略结果)
但要注意:
- 必须先
SELECT dblink_connect('conn_name', 'conn_str'),连接串里建议加application_name=task_worker方便排查 - 同一连接上不能连续调用
dblink_send_query,必须等前一个dblink_get_result()返回NULL才能发下一个 - 错误不会抛到当前函数里,得查
pg_stat_activity或日志
典型组合:
PERFORM dblink_connect('task_link', 'host=/tmp dbname=mydb');
PERFORM dblink_send_query('task_link', 'INSERT INTO log_table VALUES (now(), ''started'');');
-- 此刻已异步发出,不阻塞
Listen/Notify + dblink 是最实用的混合方案
真正落地时,推荐这样分工:
- 存储过程里用
NOTIFY发信号(轻量、快、无副作用) - 外部监听进程收到后,调用
dblink_send_query启动重任务(隔离、可控、可监控)
好处是:
- 避免在数据库里写复杂调度逻辑(PL/pgSQL 不适合)
- 监听进程可以做重试、限流、失败告警
- 任务状态可统一落库,方便前端查进度
容易被忽略的一点:NOTIFY 的 channel 名是字符串字面量,不能拼接变量(除非用动态 SQL),而动态 SQL 在 PL/pgSQL 里要走 EXECUTE,有权限和注入风险。所以 channel 名最好写死,靠 payload 区分业务类型。

















