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

更新于 · 在 sijie.xyz 查看原条目 ↗

状态: 已在 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 整个服务就能删掉。另议。