为什么不用 Kafka、RabbitMQ 或 Redis/Bull
状态: 已在 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 搬过去——只是多一跳。队列和业务在同一个库里,入队本身就是事务的一部分。
flowchart LR
subgraph broker["With a broker: still an outbox, plus one hop"]
direction LR
a1[domain write] -->|same tx| b1[(outbox)]
b1 --> c1[relay] -->|cross-process network| d1{{Kafka / RabbitMQ}}
d1 --> e1[consumer]
end
subgraph pg["Queue on Postgres: enqueue inside the tx"]
direction LR
a2[domain write] -->|same tx| b2[(outbox)]
b2 --> c2[relay] -->|same database, same tx| d2[(job table)]
d2 --> e2[consumer]
end
二、语义不对口
我们要的是任务队列:每次投递单独重试、按计划退避、持续失败停用端点。
- Kafka 是顺序日志,没有单条延迟重试,要自建重试 topic 和死信,且一条失败会卡住整个分区。
- RabbitMQ 延迟重试要靠死信交换机 + TTL 或插件拼。
三、我们的 Redis 被设计成会丢数据
docker-compose.prod.yml 配的是 256MB + allkeys-lru:满了淘汰最旧的 key。会话、限流丢了能重建;任务放上去就会静默丢失。仓库里也没有 Bull/BullMQ(后端是 Go,用 Bull 还得多一个 Node 进程)。
四、体量和成本
事件量是一天几百到几千条(语料编辑、申请、预约),Postgres 每秒几千次都轻松。自部署体验是卖点,要能跑在 1GB 小机器上。
对比
| 方案 | 新增服务 | 典型内存 | 事务性 | 单条延迟重试 | 运维 |
|---|---|---|---|---|---|
| Kafka | broker(KRaft 也要 JVM) | 1GB 起 | 双写,需 outbox | 无,靠重试 topic | 分区、保留期、磁盘、升级 |
| RabbitMQ | Erlang 服务 | 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 整个服务就能删掉。另议。