PostgreSQL 的 NOTIFY/LISTEN 是 Go 应用监听数据库变更最轻量、最实时的方案;轮询因事务可见性、事件丢失、资源开销和连接堆积等问题应避免,仅当使用 MySQL 且无法接入 binlog 服务、变更极低频时才可降级考虑。

PostgreSQL 的 NOTIFY/LISTEN 是目前 Go 应用监听数据库变更最轻量、最实时的方案;轮询在绝大多数场景下只是权宜之计,不是“备选”,而是“该避免”。
为什么不能靠轮询做实时通知
轮询本质是让 Go 程序定期执行 SELECT COUNT(*) FROM table WHERE updated_at > ? 这类语句。它的问题不是“写不出来”,而是:
- 事务未提交时查不到变更,
READ COMMITTED隔离级别下必然漏事件 - 高并发写入时,两次轮询之间可能产生多条变更,但你只拿到“有变化”这个模糊信号,丢失顺序和明细
- 每张表单独轮询 → 连接数、CPU、网络开销线性增长;加索引优化
updated_at也扛不住高频更新 -
pg_stat_activity里会堆积大量 idle in transaction 的轮询连接,DBA 第一时间就会找你
PostgreSQL 触发器 + NOTIFY 的 Go 实现要点
核心链路是:数据变更 → 触发器函数 → NOTIFY channel_name, payload → Go 连接 LISTEN → 接收消息。关键不在“怎么写触发器”,而在“Go 怎么稳住监听连接”:
- 必须用独立连接监听:
pgxpool.Pool的连接是复用的,LISTEN后该连接会被挂起,不能再用于查询;要调pool.Acquire(ctx)拿一个专属连接 - 监听连接不能复用或超时自动释放:一旦断开,中间的通知就丢了;需自己维护长连接,配合
conn.Ping(ctx)心跳检测 + 断线重连逻辑 - payload 限制 8000 字节:别直接塞整行 JSON,优先传
id和TG_OP,业务侧再查详情;或用json_build_object('op', TG_OP, 'id', NEW.id)控制字段粒度 - 频道名建议带 schema 前缀:比如
public_users_changes,避免不同 schema 下同名表冲突
Go 监听端容易崩的三个细节
很多人跑通一次就以为成了,但上线后常因以下原因静默丢通知:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
-
LISTEN后没循环读conn.Notification():PostgreSQL 不推消息到 channel,你要主动轮询连接的 notification 队列(注意这不是数据库轮询,是内存队列) - 没处理
pgconn.PgError:比如频道名含大写字母或特殊字符,LISTEN "UserEvents"会报错但被忽略,监听实际失败 - ctx 被 cancel 导致监听中断:比如 HTTP handler 的 ctx 传进来,handler 结束就断连;监听必须用 background context 或自定义 long-lived ctx
什么时候才该考虑轮询
仅当满足全部条件时可降级使用:
- 数据库是 MySQL(无原生
NOTIFY),且无法接入binlog解析服务(如 Debezium) - 变更频率极低(
- 表结构固定、有可靠的时间戳字段(
updated_at或created_at),且应用能保证该字段严格单调递增(NTP 同步、无手动 update)
即便如此,也要加一层本地缓存比对(比如记录上次最大 id),否则 WHERE updated_at > ? 在毫秒级写入洪峰下仍可能重复或跳变。

















