2026-09-27·by Sijie Wang#standmeet#architecture#design#events

why-not-a-broker

为什么不用 Kafka、RabbitMQ 或 Redis/Bull

上级:events

状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。

队列放在我们已经在跑的 Postgres 上,不加任何服务。River 是“在 Postgres 上做队列”的一种现成写法;嫌依赖多就自写精简版,栈一样不变。有四条理由排除 broker。

一、broker 省不掉 outbox

业务改动和“发出事件”必须一起成败。Kafka / RabbitMQ / Redis 都参与不了 Postgres 事务,换用它们仍得先写 outbox 再由 relay 搬过去——只是多一跳。队列和业务在同一个库里,入队本身就是事务的一部分。

二、语义不对口

我们要的是任务队列:每次投递单独重试、按计划退避、持续失败停用端点。

  • Kafka 是顺序日志,没有单条延迟重试,要自建重试 topic 和死信,且一条失败会卡住整个分区。
  • RabbitMQ 延迟重试要靠死信交换机 + TTL 或插件拼。

三、我们的 Redis 被设计成会丢数据

docker-compose.prod.yml 配的是 256MB + allkeys-lru:满了淘汰最旧的 key。会话、限流丢了能重建;任务放上去就会静默丢失。仓库里也没有 Bull/BullMQ(后端是 Go,用 Bull 还得多一个 Node 进程)。

四、体量和成本

事件量是一天几百到几千条(语料编辑、申请、预约),Postgres 每秒几千次都轻松。自部署体验是卖点,要能跑在 1GB 小机器上。

对比

方案新增服务典型内存事务性单条延迟重试运维
Kafkabroker(KRaft 也要 JVM)1GB 起双写,需 outbox无,靠重试 topic分区、保留期、磁盘、升级
RabbitMQErlang 服务150MB 起双写,需 outbox靠 DLX + TTL 拼队列 / 交换机 / 持久化配置
Redis + Bull无(但要改淘汰策略 + 持久化)复用双写,需 outbox有多一个 Node 进程
River无(现有 Postgres 多几张表)几乎为零同一事务有随数据库备份迁移;+1 个 Go 库
自写(Postgres)无几乎为零同一事务自己实现几百行自己维护(有 SKIP LOCKED 租约可起步)

换成别的方案,存储增长的问题也不会消失:Redis 队列同样会堆积(且我们的 Redis 满了是静默淘汰);Kafka 自带按时间 / 大小保留,代价是多运维一个服务。见 storage-bounds。

什么时候该换 Kafka

变成大规模多租户 SaaS、很多独立服务要消费同一条流并长期回放。到时只换 Jobs / Inspector / Runtime 背后的任务实现,以及 internal/infra/events 里的 relay,各域不感知(queue-behind-ports)。

更远的省栈选项

把会话和限流也挪进 Postgres(UNLOGGED 表),Redis 整个服务就能删掉。另议。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

why-not-a-broker