# LISTEN / NOTIFY · 数据库自带的发布订阅

> PostgreSQL 内建一套轻量 Pub/Sub：一端 LISTEN 一个通道，另一端 NOTIFY 发消息，触发器里也能 pg_notify。消息随事务走——COMMIT 后才广播。缓存失效通知、轻量任务派发、库内事件钩子，不必再为此单独架一个 Redis。

- 作者：David（道雾轩）
- 专栏：PostgreSQL 特性详解（https://daiw.org/manual/postgresql.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/postgresql/listen-notify
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# LISTEN / NOTIFY · 数据库自带的发布订阅

「数据变了，怎么让别的进程立刻知道？」——常规答案是架一个 Redis / Kafka 做消息广播，或者让应用不停轮询数据库。但如果你已经在用 PostgreSQL，它自带一套轻量的发布订阅，很多场景够用了，还免去多维护一个中间件。

## 三个动作：LISTEN、NOTIFY、pg_notify

机制简单到只有三个动作：

```sql
-- 订阅端:监听一个通道
LISTEN order_events;

-- 发布端:往通道发消息(可带 payload)
NOTIFY order_events, 'order 123 created';

-- 在函数 / 触发器里发,用函数形式(payload 可动态拼)
SELECT pg_notify('order_events', '{"id": 123}');
```

把它和触发器一拼，就是「**数据一变，自动广播**」：给表加个 `AFTER INSERT` 触发器，每插入一行就 `pg_notify` 一条。实测里正是这么做的：

```text
当前连接 LISTEN ezdata_events
建表 + AFTER INSERT 触发器,每行插入广播一条
INSERT 三条 → 触发器广播 → 监听端实时收到:
  收到: {"id": 1, "msg": "hello"}
  收到: {"id": 2, "msg": "world"}
  收到: {"id": 3, "msg": "!"}
```

数据库的写入事件，就这样实时推到了应用端，全程没有轮询。

## 关键语义：跟着事务走

LISTEN/NOTIFY 有几条必须记住的语义，它们决定了它「靠谱在哪、不能用在哪」：

- **随事务提交才广播**：`NOTIFY` 发出的消息，要等所在事务 `COMMIT` 后才真正推给监听者。事务回滚，消息也不发。这点极好——**你不会收到「其实没成功」的事件**。
- **同事务内相同消息合并**：同一事务里对同一 `(通道, payload)` 发多次，监听者只收到一次。
- **payload 有大小限制**：载荷不能超过 8000 字节。所以惯例是**只发 ID / 事件类型**，让应用收到后自己回查详情，而不是把大对象塞进 payload。

<Callout type="warn">
  最要命的一条：**LISTEN/NOTIFY 不持久化**。消息发出时**如果没有活跃的监听者，这条消息就永远丢了**——它不排队、不重放、不保证送达。所以它只适合「丢了也不致命」的通知（缓存失效、UI 刷新提示），**不适合当可靠消息队列**（订单必达、支付回调）。要「至少一次送达」，得用真正的消息队列，或在库里建一张任务表 + `SKIP LOCKED` 轮询（见 [锁与并发一章](https://daiw.org/manual/postgresql/locks)）。
</Callout>

## 它和 Redis Pub/Sub 差在哪

LISTEN/NOTIFY 常被拿来和 Redis 发布订阅比。两者都「不持久化、发出即忘」，但 PostgreSQL 版有个独特优势：**和事务联动**。

- Redis pub/sub：你得先写数据库、再发 Redis 消息，两步之间可能「数据库写成功了但 Redis 挂了」，导致事件和数据不一致。
- PostgreSQL NOTIFY：消息和数据在**同一个事务**里，`COMMIT` 一起成功、`ROLLBACK` 一起取消——**数据和事件天然一致**，不存在「事件发了但数据没落库」的裂缝。

代价是吞吐和扩展性不如专业消息中间件。所以它的定位很清楚：**中小规模、要事务一致、不想多养一个组件**时的轻量选择。

## 横向对比

| 能力 | PostgreSQL LISTEN/NOTIFY | Redis Pub/Sub | Kafka / RabbitMQ |
| --- | --- | --- | --- |
| 是否内置 | **数据库自带** | 需 Redis | 需独立集群 |
| 与数据库事务一致 | **是**（COMMIT 才发） | 否（两步不原子） | 否 |
| 持久化 / 重放 | ❌ 不持久 | ❌ 不持久 | ✅ 持久、可重放 |
| 吞吐 / 扩展 | 中小 | 高 | 很高 |
| 运维成本 | 零（已有 PG） | 一个 Redis | 一套集群 |

MySQL、Oracle、SQL Server 都**没有**这种「随事务提交的轻量广播」内置能力（Oracle 有更重的 DBMS_ALERT / 变更通知），这算 PostgreSQL 一个小而美的独特点。

## 应用场景

- **缓存失效广播**：某表更新 → 触发器 `NOTIFY` → 所有应用实例收到后清对应缓存，免掉「缓存和库不一致」的定时兜底。
- **实时 UI 推送**：后台数据变化通过 NOTIFY → WebSocket 推给前端，做「多人协作实时刷新」。
- **轻量任务唤醒**：任务表插入新行 → NOTIFY 唤醒 worker「有活了」，让 worker 从「定时轮询」变成「事件驱动 + 兜底轮询」，大幅降低空轮询。
- **库内事件钩子**：审计、指标、下游同步，用触发器 + NOTIFY 挂钩数据变更。

## 小结与下一章

LISTEN/NOTIFY 让 PostgreSQL 兼职一个「随事务一致的轻量消息总线」：数据变更能实时推给应用，还不必多养一个 Redis。记住它的边界——**不持久、没监听者就丢**，所以只做「丢了不致命」的通知，别当可靠队列。

说到「可靠队列」，PostgreSQL 其实能用另一套机制做出来——**行级锁 + `SKIP LOCKED`**。这就引出并发这一站的主题：锁。下一章讲咨询锁和行级锁，看 PostgreSQL 怎么兼任分布式锁和任务队列。👉 [锁与并发：咨询锁与 SKIP LOCKED](https://daiw.org/manual/postgresql/locks)。

## 参考文献

- [1] PostgreSQL 官方文档.「NOTIFY.」https://www.postgresql.org/docs/current/sql-notify.html —— 通道、payload、事务语义、去重、大小限制。
- [2] PostgreSQL 官方文档.「LISTEN / 9.28. Notify Functions(pg_notify).」https://www.postgresql.org/docs/current/sql-listen.html —— 订阅与函数式发送。
