NOTIFY 仅发送会话级消息,不触发异步任务;真异步需外部监听进程或 dblink_send_query。常见错误是误以为 NOTIFY 会自动执行后续逻辑,实际它只向内存队列写入通知,无连接、SQL 或进程创建能力。

NOTIFY 不能在存储过程中触发异步任务,它只是发一条会话级消息;真异步必须靠外部监听进程或 dblink_send_query。
为什么存储过程里写 NOTIFY 后任务没执行
常见错误现象:NOTIFY 执行完函数立刻返回,但预期的更新、发邮件、调用 API 等后续逻辑完全没发生。
原因很直接:PostgreSQL 的 NOTIFY 不启动新连接、不执行 SQL、不 fork 进程,它只往内存消息队列里塞一条通知——仅此而已。
关键限制点:
-
LISTEN必须在NOTIFY之前注册,且监听进程需保持长连接;应用一断开,监听就失效 - 消息不持久化:监听进程离线期间发的
NOTIFY全部丢失,PG 不落盘、不重发 -
payload最大 8000 字节,超长内容得先写表,再在通知里传 ID - psql 客户端要手动执行任意语句(如
SELECT 1)才会刷出通知,API(如asyncpg)可实时回调
怎样让 NOTIFY 真正驱动后台任务
本质是“数据库事件驱动 + 外部 worker”模式,PG 不提供内置 worker,你得自己搭。
推荐做法:
- 用
asyncpg写监听服务:调用conn.add_listener('channel', callback),收到通知后用新连接执行业务 SQL(不能复用监听连接) - 用 Node.js +
pg-listen:监听到NOTIFY后触发client.query(),注意控制并发和错误重试 - 监听进程必须长连接、心跳保活;每次收到通知都新建连接执行任务,避免阻塞监听循环
- 别用
pg_cron轮询兜底——延迟高、浪费连接、无法保证顺序
示例(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))
想在存储过程内发起不等待的 SQL,用 dblink_send_query
这是唯一能在 PL/pgSQL 函数里真正实现“发起即返回”的方式。
它通过 dblink 建立另一个数据库连接,把 SQL 提交到服务端后台运行,当前函数立即继续执行。
要点:
- 需提前安装
dblink扩展:CREATE EXTENSION dblink - 连接串里不要写密码,改用
.pgpass或连接参数password=xxx(注意安全) - 任务执行与客户端是否断连无关,但结果需用
dblink_get_result主动轮询获取(或忽略) - 不适用于高并发短任务——每个
dblink_send_query占一个 backend 连接,容易打满
更稳妥的异步方案:任务表 + 守护进程
比依赖 LISTEN/NOTIFY 更可靠,尤其对任务状态、失败重试、超时处理有要求的场景。
操作路径:
- 存储过程只做一件事:
INSERT INTO task_queue (job_type, payload, status) VALUES ('send_email', '{"to":"a@b.c"}', 'pending') - 外部守护进程(Python +
psycopg2)定时轮询:SELECT * FROM task_queue WHERE status = 'pending' LIMIT 10 - 拿到任务后,用
UPDATE ... SET status = 'running' WHERE id = ...加锁,再执行业务逻辑 - 成功则设为
'done',失败设为'failed'并记录error_msg
注意点:
- 轮询间隔别太激进,5s 起步;可用
pg_sleep()配合指数退避 - 任务表必须有
created_at和updated_at,否则无法识别卡死任务 - 别用
pg_notify()触发轮询——它和NOTIFY语义完全一样,不解决根本问题
真正容易被忽略的是消息丢失边界和连接生命周期管理:NOTIFY 不是 MQ,LISTEN 不是订阅服务,任何把它们当 RabbitMQ 用的尝试,都会在压测或网络抖动时暴露问题。










