← 中文资源

简体中文版本

Outbox 模式

事务性 Outbox 把业务变更与事件写在同一数据库事务中,再由中继发布,避免双写不一致。

阅读英文原文

双写问题

服务创建订单后必须发布 OrderCreated 事件。如果先提交订单再发布,两步之间崩溃会丢失事件;如果先发布再提交,事务失败会让消费者看到不存在的订单。单独重试发布或提交都解决不了,因为两个系统不共享事务。

Outbox 把事件放进与业务数据相同的数据库。事务同时插入订单和事件行。要么都存在,要么都不存在。中继随后发布事件,失败就重试。提交与发布之间的间隙变成延迟,而不是丢失。

CREATE TABLE outbox (
  id           bigserial PRIMARY KEY,
  aggregate_id text NOT NULL,
  event_type   text NOT NULL,
  payload      jsonb NOT NULL,
  created_at   timestamptz NOT NULL DEFAULT now(),
  published_at timestamptz
);

BEGIN;
INSERT INTO orders (id, customer_id, total)
VALUES ('order-1', 'customer-9', 4200);
INSERT INTO outbox (aggregate_id, event_type, payload)
VALUES ('order-1', 'OrderCreated', '{"orderId":"order-1","total":4200}');
COMMIT;

业务写入和事件插入在同一事务中。Outbox 插入应与业务变更位于同一仓储或工作单元;有时记得写 Outbox、有时忘记的服务方法就是潜在缺陷。

中继与消费

中继认领未发布行,发布后标记为已发布。轮询简单但增加延迟和数据库负载;变更数据捕获读取数据库日志,扩展性更好,但运维复杂。无论使用哪种机制,中继都必须幂等:同一行发布两次可以接受,丢失不行。

消费者必须按事件 id 或业务键去重。保存已处理 id 的保留窗口至少与 broker 可能重投的窗口一样长。顺序是每个聚合内保证,不是全局保证;不要假设不同订单的事件有固定顺序。如果聚合内顺序重要,加入序列号并拒绝或缓冲乱序事件。

运维关注

监控最旧未发布行的年龄、发布失败和消费者滞后。健康的 Outbox 积压很小且有界。积压超过阈值,或某行超过投递 SLO 仍未发布,应告警。通过保留策略清理已发布行;审计要求需要时归档负载。

Outbox 负载应包含消费者无需回调生产者就能行动的信息,但不应携带密钥或无界文档。事件模式要版本化并兼容演进。事件是公开契约;不版本化就修改会破坏你无法控制的消费者。

何时使用

当本地数据库变更必须可靠触发外部事件,且丢失或凭空产生事件不可接受时,使用 Outbox。优点是不用分布式事务也能原子化;缺点是额外存储、需要运维中继,以及至少一次投递要求消费者去重。与在应用代码中直接发布相比,Outbox 增加延迟和基础设施,但消除了双写故障模式。如果偶尔丢失事件可以接受,且事件只是提示性的,直接发布加重试可能更简单。