首页/数据/NoSQL 速查/事务外盒:可靠事件发布
选型与架构50 条

事务外盒:可靠事件发布

业务写入与事件发布要原子:在同一事务里写业务表和 outbox 表,再由中继任务读取 outbox 投递消息,投递成功后标记或删除。这样避免"数据库提交了但消息没发出去"的丢事件问题,代价是引入中继与幂等消费。

NoSQL 速查数据更新于 2026-10-13

外盒模式解决什么

业务写入与事件发布要原子:如果在同一事务里写业务表与 outbox(外盒)表,再由中继任务读取外盒投递消息,就不会出现「数据库已提交但消息没发出去」的丢事件问题。代价是引入中继与至少一次的投递语义,消费端必须幂等,同时要监控外盒积压。

外盒流程(MongoDB 事务示意)

外盒流程(MongoDB 事务示意)
// 副本集或分片集群事务内:业务写入 + 外盒事件
const session = db.getMongo().startSession();
session.startTransaction();
try {
  session.getDatabase("app").orders.insertOne(
    { _id: 1001, status: "paid", amount: 128 },
    { session }
  );
  session.getDatabase("app").outbox.insertOne(
    { eventId: UUID(), type: "order.paid", payload: { orderId: 1001 }, status: "pending" },
    { session }
  );
  session.commitTransaction();
} catch (e) {
  session.abortTransaction();   // 任一步失败则整体回滚,不会出现「写库成功但事件缺失」
  throw e;
}

// 中继:轮询 pending 并投递,成功后标记 sent(至少一次投递)

基本信息

中继实现轮询并按条件抢占更新,或直接消费外盒集合的变更流
投递语义至少一次,消费端按事件 ID 幂等去重
并发保护抢占式更新要带状态条件,避免多个中继重复投递同一事件
可观测外盒积压量、最旧未投递事件的时间、投递失败次数
有 CDC 时由变更流直接消费数据变更可省去外盒,但事件格式与过滤条件要在日志层设计

典型使用场景

  • 订单状态变更通知:保证「状态改了」与「通知发出」不脱节
  • 跨服务数据同步:可靠地把变更投递给搜索、缓存与下游系统
  • 对账与审计:事件与业务数据成对留存,便于追溯
  • 异步任务触发:支付成功后可靠触发发货、积分等后续流程
  • 灰度迁移:新旧双写期间靠外盒事件驱动新链路补数据

环节与失败处理

环节失败可能处理方式
事务内双写业务写成功、事件未写同事务提交,任一步失败整体回滚
中继投递投递成功但标记失败至少一次语义,消费端按事件 ID 去重
中继抢占多个中继重复处理同一事件按状态条件原子更新抢占,加超时回收
外盒清理已投递事件长期堆积定期归档或删除已投递记录并保留审计所需窗口
积压监控中继停摆无人发现对积压量与最旧事件时间设置告警
外盒事件建议带唯一事件 ID 与业务主键:消费端用唯一索引去重,重复投递不会产生重复副作用。
中继与业务不要共用同一个低优先级连接池并无限重试:投递积压时反向拖慢业务库,故障从下游扩散到核心链路。

易错点

  • 先提交业务再发消息:发送失败时事件丢失,下游永远不知道这笔变更
  • 中继与业务共用连接池且无限重试:投递积压反过来拖慢业务库
  • 外盒积压没有监控:中继停摆数小时后才由业务方发现,补偿成本成倍上升
  • 消费端不做去重:至少一次投递必然带来重复事件,重复副作用难以排查与修复

数据来源与参考

  • MongoDB Manual: Transactions
  • microservices.io: Transactional outbox(模式参考)
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌