PostgreSQL LISTEN/NOTIFY 性能真相:从全局锁瓶颈到 20 倍吞吐量优化
PostgreSQL 的 LISTEN/NOTIFY 是一个被低估的内置功能。它让数据库直接充当消息队列,支持低延迟的发布/订阅模式,对于流式传输、实时通知、LLM token 流推送等场景非常实用。但它有一个长期困扰开发者的性能问题:全局排他锁(global exclusive lock),所有包含 NOTIFY 的提交事务被强制串行化执行。2025 年 3 月,会议记录平台 Recall.ai 因这个锁导致数据库三次宕机,随后发表了一篇广泛流传的文章,标题直接叫"Postgres LISTEN/NOTIFY does not scale"。
2026 年 7 月,数据库基础设施公司 DBOS 发表了针锋相对的回应:"Postgres LISTEN/NOTIFY Actually Scales"。他们通过缓冲批量提交策略,将单个 Postgres 实例上的流写入吞吐量从 2900 次/秒提升到 60000 次/秒,延迟保持在 15 到 100 毫秒之间。这个 20 倍的性能提升登上了 Hacker News 首页。
这篇文章深入分析 LISTEN/NOTIFY 性能瓶颈的技术根源、DBOS 的优化方案,以及开发者在实际使用中需要权衡的取舍。
LISTEN/NOTIFY 的设计机制
PostgreSQL 的 LISTEN/NOTIFY 提供了数据库原生的发布/订阅通信。客户端通过 LISTEN channel_name 订阅频道,另一个客户端通过 NOTIFY channel_name, 'payload' 发布消息。所有正在监听该频道的客户端会收到通知。
这个机制最典型的应用场景是流式数据传输。以 LLM 逐 token 输出为例:每次模型生成一个新 token,将其作为一行插入 streams 表,同时通过 NOTIFY 通知读者有新数据到达。读者不需要反复轮询数据库,而是在收到通知后立即读取新行。这比轮询方案高效得多——轮询间隔太长会损害交互体验,太短则给数据库带来过大压力。
在 DBOS 的初始实现中,他们在 streams 表上设置了一个触发器,每次插入新行时自动调用 NOTIFY。这在功能上完全正确,延迟也很低,但高并发场景下的吞吐量很差:即使使用大型 Postgres 实例,也无法超过 2900 次/秒的写入速度。更奇怪的是,瓶颈发生时 CPU、内存和磁盘 IOPS 的利用率都很低。
全局排他锁:瓶颈的根源
性能瓶颈来自一个 PostgreSQL 官方文档中从未提及的内部机制。
当事务中调用了 NOTIFY 时,该事务在提交阶段需要获取一个全局排他锁。这个锁从事务开始提交时获取,一直持有到事务完全提交完毕,包括将数据刷入磁盘的 fsync() 调用。在此期间,数据库实例中任何其他包含 NOTIFY 的事务都无法提交。
PostgreSQL 这样设计的原因是保证通知的发送顺序严格匹配事务的提交顺序。所有待发送的通知被存储在一个全局内部队列中,队列顺序必须与发送这些通知的事务的提交顺序完全一致。然而,PostgreSQL 直到事务提交完成后才确定其提交顺序,因为不同事务的提交耗时各不相同。这就产生了一个排序困境:包含通知的事务必须按提交顺序加入队列,但提交顺序直到提交完成后才确定。PostgreSQL 的解决方案就是用全局锁将所有包含 NOTIFY 的提交串行化,使得它们的提交顺序在提交前就已经确定。

这个锁直接破坏了 PostgreSQL 最关键的并发优化之一:组提交(group commit)。正常情况下,PostgreSQL 会将多个并发提交的事务合并到一次 fsync() 调用中完成,这在高写入场景下对吞吐量至关重要。但当 NOTIFY 锁强制串行化提交后,每个事务必须独立进行一次完整的 fsync 刷盘。这解释了为什么瓶颈发生时系统资源利用率很低:CPU 和磁盘实际上处于空闲状态,所有事务都在排队等待全局锁。
Recall.ai 的生产环境记录直观展示了这个问题。2025 年 3 月 22 日,他们的数据库日志显示,一个进程等待 AccessExclusiveLock 已超过 1000 毫秒,等待队列中有 68 个进程排在它前面。所有这些进程都在等待同一个锁释放,数据库实际吞吐量急剧下降。
DBOS 的优化方案:缓冲批量提交
DBOS 团队的核心洞察是:对于流式传输应用,通知本身不是数据的真实来源。通知只是一个信号,告诉读者去查数据库表(真正的数据源)获取新数据。因此,通知不需要严格的全局排序和完美的持久性保证。
基于这一洞察,他们的优化策略是:将通知缓存在内存中,然后周期性地用单个批量事务将缓冲区中的所有通知一次性提交。全局锁只在批量提交时获取一次,而不是每次写入都获取。这样,单个流写入可以快速完成,利用 PostgreSQL 的组提交优化实现高吞吐量,缓冲区在后台异步刷新。
缓冲机制引入了一个新的风险:如果进程在通知尚未刷新时崩溃,这些通知将永远不会被送达。DBOS 的解决方案是为流读取者增加一个回退机制:除了等待通知外,读取者还会周期性地轮询数据库,检查是否有新数据写入但未收到通知。由于这个轮询只是处理未送达通知的兜底手段,频率可以很低,对整体性能影响微乎其微。
Benchmark 数据对比
优化前后的性能差距非常显著。
DBOS 公布的 benchmark 使用 64 个写入进程、每个进程 16 个并发流、连接池大小 20,在 64 个读取进程的压力下运行 900 秒。优化前的触发器方案在每秒约 2900 次写入时饱和,p50 延迟在吞吐量超过 2500 后从约 10 毫秒飙升到 130 毫秒以上,p99 延迟从约 18 毫秒暴增到 190 毫秒。优化后的缓冲批量提交方案将吞吐量提升到每秒 60000 次写入,p50 延迟保持在 15 毫秒左右,p99 延迟在 100 毫秒以内。在最极端的吞吐量下,PostgreSQL 的 CPU 利用率接近 100%,说明数据库确实在满负荷运转,不再被锁竞争限制。
| 指标 | 优化前(触发器方案) | 优化后(缓冲批量方案) |
|---|---|---|
| 最大吞吐量 | ~2,900 writes/sec | ~60,000 writes/sec |
| p50 延迟(正常负载) | ~10ms | ~15ms |
| p99 延迟(正常负载) | ~18ms | ~100ms |
| 瓶颈类型 | 全局锁竞争 | CPU 满载 |
| fsync 行为 | 每事务独立刷盘 | 批量合并刷盘 |
所有 benchmark 代码在 GitHub 开源:dbos-inc/dbos-postgres-benchmark。
Postgres 19 的相关补丁
PostgreSQL 社区也意识到了这个问题。即将发布的 PostgreSQL 19 包含了一个与 NOTIFY 相关的补丁。但 DBOS 的文章特别指出,这个补丁并没有移除全局锁或修复上述瓶颈。它优化的是一个更窄的场景:当存在大量通知频道,且每个监听者只等待特定频道时。对于 DBOS 所处理的流式传输场景(所有写入者都在同一个频道上),这个补丁没有实质帮助。
这意味着在 PostgreSQL 19 中,LISTEN/NOTIFY 的全局锁问题仍然存在,开发者仍需要通过应用层优化来规避它。
开发者的实际选择
面对 LISTEN/NOTIFY 的锁瓶颈,开发者有几条路径。
第一条路径:保留 LISTEN/NOTIFY,在应用层做缓冲。 这是 DBOS 采用的方案,适用于通知频率高但通知本身只是"提醒去看数据"的场景。核心思路是将每个写入都触发 NOTIFY 改为批量触发,减少对全局锁的争用。代价是通知延迟略有增加(取决于批量刷新间隔),以及需要一个兜底轮询机制处理缓冲区中可能丢失的通知。
第二条路径:完全放弃 LISTEN/NOTIFY,迁移到应用层。 这是 Recall.ai 的选择。他们在 2025 年 3 月发现全局锁导致三次宕机后,在不到一天的时间内将相关逻辑迁移到应用层处理,此后数据库运行稳定。对于写入并发量高、通知可靠性要求严格的系统,直接绕过数据库级别的通知机制可能更安全。
第三条路径:使用专用消息队列。 当吞吐量需求超过单机 Postgres 的承受能力时,引入 Redis、RabbitMQ 或 NATS 等专用消息中间件是常规做法。PostgreSQL 适合在中等吞吐量场景下一站式解决问题,但在超大规模下,专用工具的架构设计天然适合高并发消息传递。
选择哪条路径取决于具体场景。如果 Postgres 已经是系统的核心组件,通知负载在每秒数千到数万次之间,DBOS 的缓冲方案能让你在不引入新组件的前提下大幅提升性能。如果写入并发量极高或通知可靠性至关重要,绕过 LISTEN/NOTIFY 可能是更理性的决策。
LISTEN/NOTIFY 仍然适合的场景
尽管存在全局锁问题,LISTEN/NOTIFY 在以下场景中仍然是一个优秀的选择:
- 低写入并发、低延迟需求:当每秒只有几十到几百次写入时,全局锁几乎不会成为瓶颈,LISTEN/NOTIFY 提供的即时通知能力远优于轮询方案
- 数据库事件驱动的缓存失效:当数据变更时通知应用层清除缓存,写入频率通常不高
- 开发阶段的原型设计:不想引入 Redis/RabbitMQ 等额外组件时,直接用 Postgres 内置功能快速搭建原型
- PostgreSQL 作为唯一基础设施的轻量级应用:简化运维,避免管理多个中间件
关键判断标准是写入并发量。Recall.ai 的经验表明,当并发写入者数量达到数十个时,全局锁开始造成可感知的延迟;当写入者继续增加时,锁等待队列快速增长,最终导致数据库响应时间飙升。DBOS 的缓冲方案将这条边界线从每秒 2900 次推到了 60000 次,但没有消除它。
PostgreSQL 的 LISTEN/NOTIFY 是一个设计精巧的功能,它的全局锁是一个工程妥协而非设计缺陷——用确定性换取简单性。DBOS 的优化证明了,只要理解瓶颈的底层机制并针对性优化,这个功能可以在更高的吞吐量下继续发挥作用。而 Recaa.ai 的迁移经历则提醒我们,当某个机制成为系统瓶颈时,勇于绕过它同样是正确的工程决策。