想象你经营着一家老式邮局。营业员小王每天要给客户寄出“业务已办结”的通知信。他先在厚厚的登记簿上写下客户的订单信息,然后转身跑到门口,把信投进邮筒。可问题来了:如果在他跑向邮筒的路上突然停电,登记簿上记了订单,但信还没投出去;或者信投出去了,登记簿还没写。客户那边可能收到信却查不到订单,系统里的账目和现实中的信件完全对不上。这就是微服务世界里最常见的“分布式事务”噩梦——业务数据库和消息队列是两个独立的系统,你想同时完成“记账”和“寄信”,却没有任何一双手能同时锁住两个地方。 Outbox Pattern 给出的解决方案特别朴素:在小王办公桌旁边放一个带锁的抽屉(Outbox 表),他记账的同时把要寄的信也塞进这个抽屉,账本和抽屉都在同一间办公室、共用同一把锁(本地数据库事务),要么一起成功,要么一起回滚。之后再由专门的邮差(发布器)定时打开抽屉,把信取走投递。这封信不会丢,因为信封上还印了唯一的流水号,即使送重了,收件人也能认出来。本文就用这个“邮局抽屉”的比喻,带你一层层拆开 Outbox Pattern 在微服务事务中的关键作用。
对 AI 从业者而言,理解 Outbox 能让你在构建实时特征管道、智能风控与推荐系统时,保证业务事件与模型输入严格一致——避免“脏数据喂给 AI”导致错误预测,这是从单机模型走向生产级 AI 系统的必修课。
目录
可视化演示
展示Outbox模式两阶段事务流程、传统方案一致性对比及轮询与CDC实现性能差异。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Outbox两阶段事务流程 阶段1:业务表+Outbox表同事务写入 阶段2:Relay读取Outbox发布到MQ TX(Biz,Outbox)→Relay→MQ 原子性保证事件不丢失
问题困境:账本与信件的世纪难题
老邮局里,营业员小王每天都要给客户寄出“业务已办结”的通知信。他先把客户订单写进登记簿,再跑到门口把信投进邮筒。这两个动作一个落在桌面上,一个落在门口,中间隔着一整条走廊。只要走廊上有一点意外——停电、门被锁、邮筒装满——就会出现“账记了但信没寄”或“信寄了但账没记”的尴尬局面。微服务架构下的双写难题,本质上就是小王这段走廊的距离。
在分布式系统里,“登记簿”是订单数据库,“邮筒”是消息中间件(Kafka、RabbitMQ 等),“寄信”是向其他服务发布领域事件。业务操作和事件发布分属两个进程,跨系统进行原子提交几乎不可能。传统方案要么使用两阶段提交(2PC),代价高、性能差;要么依赖消息中间件提供的事务消息,但这种能力并非所有 MQ 都支持。于是人们想到:既然跨系统无法保证原子性,那就把“发布动作”先降级为“记录动作”,让记录与业务操作挤进同一个本地事务。
这就是 Outbox Pattern 的出发点:要寄的信先进抽屉,抽屉与账本共处一室。数据库本地事务天然具备原子性——要么都提交,要么都回滚。我们可以用一个数学式子描述这个“全有或全无”的承诺:
小王终于不用再跑那段走廊了。他把账本和信一起锁进柜台保险柜,柜门一开,要么两样都在,要么两样都锁着。微服务世界的“账本”和“信”,第一次在同一个事务里获得了一致的命运。
- 本地事务记号
T_local表示这一次数据库事务的执行结果,1 代表成功提交,0 代表失败回滚。 - 成功条件业务表的插入和 outbox 表的插入必须同时提交,两者用“且”(∧)连接才算成功。
- 失败回滚任一个插入失败,整个事务回滚,数据库不保留任何一半的结果。
核心原理:抽屉与账本同锁
小王的新工作流程是这样的:客户来办业务,他在账本上写下一行订单记录,同时抽出一张“待发信卡”,填上收件人、内容和流水号,塞进办公桌右侧的抽屉。这两件事发生在同一张桌子上,共用同一把抽屉锁——数据库事务。只要锁扣上,抽屉里的信和账本上的记录就一起被保存;如果中途发现填错了,锁打开,两个动作一起撤销。
从技术视角看,“抽屉”就是数据库里的 outbox 表。它记录的事件需要包含几个关键字段:事件 ID(唯一编码)、业务聚合 ID、事件类型、业务载荷(payload)、状态位(pending 表示待发,sent 表示已发)。当业务请求到达时,应用程序在一个数据库事务中同时执行两条 INSERT:一条写入业务表(如订单表),一条写入 outbox 表。事务提交后,业务数据和待发布事件就形成了不可分割的绑定关系。
这个设计的美妙之处在于它不依赖任何外部组件,只用数据库本身的能力就解决了“业务与事件的一致性”。无论后续发布器何时读取,outbox 表都忠实地记录着所有“应该被通知出去的事实”。用一条守恒公式来看,事件总量永远守恒:
抽屉只是一个中转站,不是信的终点。信在抽屉里躺得再久,最终也要由邮差取走,才能被客户签收。Outbox 表负责“可靠地暂存”,而“可靠地投递”则由发布器来完成。
- 待发送计数
N_pending表示 outbox 表中状态为“待发送”的事件条数。 - 已发送计数
N_sent表示状态为“已发送”的事件条数,两者共同构成事件的完整集合。 - 总量守恒两者之和等于事件总数,任何时刻都不会凭空多出一条或少掉一条事件。
发布机制:邮差如何取信
邮局每天都要处理源源不断的信件,小王不可能每塞进一封信就亲自跑一趟邮筒。他安排了一位专职邮差,邮差有两种取信方式:第一种是固定时间打开抽屉,把里面的信一次性清空带走——这叫定时轮询;第二种是抽屉装了感应铃,信一放进去铃就响,邮差立刻来取——这叫变更数据捕获(Change Data Capture,简称 CDC)。
定时轮询实现简单,应用程序定期执行 SELECT ... FROM outbox WHERE status='pending',把批量的信取出来发送到消息队列。但它的缺点是存在轮询周期带来的延迟,而且频繁查询会给数据库带来额外压力。CDC 则通过解析数据库的 binlog 或 WAL 日志,实时感知 outbox 表的新增记录,再由 Debezium、Canal 等工具把变更流转发到消息队列,延迟更低,但需要额外引入基础设施。
无论是哪种方式,发布器都可能遇到投递失败:网络抖动、MQ 宕机、消息格式错误。这时候就需要重试,但不能像莽撞的邮差一样每秒疯狂敲门,而应该采用指数退避——每次重试的间隔时间翻倍,给系统恢复的时间:
邮差虽然偶尔要重跑,但因为有退避策略,不会在系统还没恢复时把邮局大门敲坏。发布器负责把信从抽屉里取出来,至于收件人怎么处理这封信,是下一环节的故事。
- 初始等待
t0是第一次重试前等待的基础时长,例如 1 秒。 - 重试次数
n表示当前是第几次重试,从 1 开始计数。 - 指数增长每次重试的间隔约为上一次的两倍,避免瞬时重试风暴打垮对方系统。
消费保证:收件人如何不重复拆信
邮差把信送到客户手里,但客户发现同一封信被送了两次——第一次投递时对方网络中断没收到确认,邮差不得不重新投递。好在每一封信的信封上都印着唯一的流水号,客户看到这个号码已经签收过,就直接把第二封信归入存档,不再打开处理。这就是“幂等消费”的日常体现。
消息队列的投递语义通常是“至少一次”(at-least-once),这意味着消费者收到重复消息是正常现象,而不是异常。如果消费者不做任何防护,每收到一条重复消息就执行一次业务逻辑,就会造成重复扣款、重复发券、重复建单等严重事故。Outbox 记录中天然携带的事件 ID 或“聚合 ID + 版本号”就是信封上的流水号,消费者可以把它作为唯一业务键,在本地数据库建唯一索引,或者在缓存中保存已处理 ID 集合,实现去重。
一个合格的事件消费者,必须在处理前回答一个问题:“这个事件我是不是已经处理过了?”唯一性约束给出了数学上的保证:
信可以重送,但不能重复拆。即便邮差手抖、网络重试、消息队列发生重复投递,只要收件人牢牢守住“唯一编号”这道防线,业务账目就永远不会乱。
- 全称限定
∀读作“对于所有”,说明约束适用于事件集合中的每一条消息。 - 唯一性判据
id(m1) ≠ id(m2)要求任意两条事件的 ID 不能相同,这是幂等判定的基础。 - 去重动作消费者只需检查 ID 是否已存在,存在则跳过处理,不存在则处理并记录。
性能优化:忙时邮局怎么扛
到了双十一,邮局的信件量暴增。如果所有信都塞进同一个抽屉,小王找信要翻半天,邮差取信也要排队。他做了三件事:第一,把抽屉按街道分成多个格子,不同的街区对应不同的格子——这叫分区;第二,邮差不是一封一封地送,而是一捆一捆地搬——这叫批量;第三,在门口挂了一块告示牌,实时显示“待处理件数”,一旦积压超过红线就加派人手——这叫监控。
对应到 Outbox 系统,分区让 outbox 表可以按业务租户、订单号哈希、事件类型等维度拆分为多个物理分区,降低单一表的锁竞争和 IO 压力。批量让发布器每次读取 100 条甚至 1000 条待发事件,一次性发送到 MQ,显著提升吞吐。监控则盯住三个核心指标:pending 数量、发布延迟、消费失败率,一旦积压超过阈值立即告警扩容。此外,别忘了清理已发送的记录——如果 outbox 表无限膨胀,查询性能会越来越差,通常采用定时删除或归档到冷表的方式。
这些优化背后有一个经典的量化关系:系统内的积压事件数等于事件到达速率乘以事件在系统中逗留的时间。这就是利特尔定律:
只要“新信到达速率”和“邮差处理速率”相匹配,等待队伍就不会爆仓。性能优化的本质,就是调整 λ 和 W 这两个旋钮,让 L 始终保持在可控范围内。
- 积压量
L代表在任意时刻 outbox 表中待发事件的平均数量。 - 到达速率
λ表示单位时间内新写入 outbox 表的事件数,例如每秒 50 条。 - 逗留时间
W表示一条事件从写入到发布成功所经历的平局秒数。 - 相乘关系积压量等于速率乘以时间,三者构成动态平衡。
常见误区:别把抽屉当垃圾桶
有些人以为有了 Outbox 抽屉就万事大吉,什么消息都往里面塞,连过期的广告宣传单也放进去;还有人认为抽屉里的信放着不动也没关系,反正不会丢。这些想法就像把抽屉当成了垃圾桶,最终导致整个邮局弥漫着腐烂的气味。Outbox 不是存储系统,更不是消息中间件的替代品,它只是一条短暂中转的传送带。
与 Outbox 相关的“极致诱惑”很多,最常见的误解包括:认为 Outbox 能保证消息一定被成功消费、认为发布失败可以无限重试、认为加了 Outbox 就不需要幂等设计。实际上,Outbox 只解决了“业务写入与事件记录原子落地”这一个问题,后面还有一长串链路需要守护。如果发布器重试没有上限,一旦 MQ 长时间故障,重试请求会像雪崩一样压垮系统;如果消费者不幂等,重复投递依然会造成业务数据错乱。
关于重试,工程上有一个安全边界:当累计等待时间超过最大容忍时间时,就应该停止重试并转入人工处理。最大重试次数可以由以下公式推算:
信箱是有纪律的通道,不是垃圾堆。越是可依赖的工具,越要清楚它的边界——Outbox 只负责把“事实”可靠地送到门口,后续的投递、签收、归档每一步都需要各自的规则。
- 容忍上限
T_max是系统允许的总等待时间,比如 10 分钟。 - 比例换算用最大容忍时间除以初始等待时间,得到总等待时间相当于多少个初始间隔。
- 对数取整取以 2 为底的对数并向上取整,换算成达到该总时长需要的翻倍次数。
- 加上首重再加 1 补上第一次尝试本身,得到总的重试次数上限。
场景实践:电商订单的实战之旅
顾客在电商平台下单的那一刻,邮局不同楼层开始联动:订单服务在 3 楼记账,库存服务在 5 楼扣减,积分服务在 8 楼加积分,推荐系统在 10 楼刷新用户画像。过去它们彼此喊话(直接调用接口),一喊就得等,一断就全乱。有了 Outbox 这根贯穿楼层的可靠管道,订单服务只需要在本地事务里完成“写订单 + 塞信”,然后由发布器把"OrderCreated"这封信送到各楼层。
更完整的链路是这样的:用户提交订单,订单服务在同一个数据库事务里插入订单表记录和 outbox 事件记录;发布器扫描到该事件后,将其投递到 Kafka 的 order-topic;库存服务、积分服务、推荐系统作为消费者订阅这个 topic,各自以 event_id 做幂等去重后执行本地逻辑。支付回调成功时,同样的机制再发送一遍"PaymentSuccess",驱动发货通知和财务对账。所有环节的最终一致性,都能通过核对事件总数来验证。
在电商和金融系统里,对账公式永远是最后的保险。每一条订单记录都应当对应一条可追踪的事件记录,支付金额和履约金额也必须落在同一个平衡点上:
当顾客看到“支付成功”与“包裹已出库”同时成立时,背后是无数封带着唯一编号的信,安静而有序地穿过整栋大楼的楼梯。Outbox Pattern 没有制造奇迹,它只是用一个小小的抽屉,把微服务世界里最令人头疼的“一致性”难题,变成了一道道清晰可查的轨迹。
- 收入侧累计
Σ payment_amount是所有已确认支付订单的金额总和。 - 支出侧累计
Σ shipment_amount是所有已发货订单的履约金额总和。 - 差值归零两者相减的差值 Δ 必须等于 0,说明支付与履约完全对得上。
AI 应用场景
实时特征管道:让推荐模型吃到“同一封信”
阶段:数据工程 / 特征平台
解决的问题:推荐系统需要订阅订单、浏览、点击等业务事件来更新用户特征。传统做法依赖业务方直接发送 MQ 消息,一旦消息丢失,特征就永远缺失,模型会基于不完整的历史做出偏差推荐。Outbox 将业务事件的可靠落库与发布解耦,保证特征平台收到的每条事件都有据可查。
具体案例:某电商推荐平台的用户向量实时更新模块,通过订阅 OrderCreated 与 CartUpdated 事件重建用户兴趣图谱。引入 Outbox 后,订单库与事件流不再出现“有订单无事件”的缺口,向量更新延迟从分钟级降到秒级,推荐 A/B 实验的置信度显著提升。
智能风控:为每一笔交易盖上可信戳
阶段:模型推理 / 实时决策
解决的问题:风控模型在用户支付瞬间进行欺诈判定,需要支付、订单、设备等多维度事件同时到位。若事件链路存在丢失或重复,模型可能误杀正常交易或放过风险交易。Outbox 模式保证支付结果事件与业务状态严格一致,让风控模型面对的是完整且无重复的历史序列。
具体案例:某支付公司的反欺诈系统使用 Outbox + Kafka 接入支付成功与退款事件。事件 ID 在入库时作为唯一键,重复投递被幂等层拦截。风控模型的特征窗口因此获得干净数据,欺诈召回率提升约 12%,误报率下降 5%。
特征回填与事件溯源:训练样本不再残缺
阶段:模型训练 / 样本生成
解决的问题:训练样本需要从业务日志中提取标签与特征,如果标签事件和状态事件不同步,样本会出现“正样本无对应标签”的脏数据。Outbox 可以作为事件溯源的基础设施,让所有业务事实以追加方式可靠保存,便于离线回填和重建训练集。
具体案例:某电商智能客服团队需要根据订单取消、售后申请事件构造训练样本。通过阅读 Outbox 表的历史轨迹,团队快速回填了过去 90 天的完整事件序列,样本覆盖率从 72% 提升到 98%,模型在意图识别任务上的 F1 值提升了 0.09。
Python 演示
import sqlite3import uuid DB_PATH = ":memory:" def init_db(): conn = sqlite3.connect(DB_PATH) conn.execute("CREATE TABLE IF NOT EXISTS orders (id TEXT PRIMARY KEY, amount REAL, status TEXT)") conn.execute("CREATE TABLE IF NOT EXISTS outbox (id TEXT PRIMARY KEY, event_type TEXT, payload TEXT, status TEXT DEFAULT 'pending')") conn.commit() return conn def place_order(conn, order_id, amount): # 同一本地事务内写业务表与 Outbox 表 with conn: conn.execute("INSERT INTO orders VALUES (?, ?, ?)", (order_id, amount, "CREATED")) event_id = str(uuid.uuid4()) payload = '{"order_id":"' + order_id + '","amount":' + str(amount) + '}' conn.execute("INSERT INTO outbox VALUES (?, ?, ?, ?)", (event_id, "OrderCreated", payload, "pending")) return event_id def publish_batch(conn, batch_size=3): # 发布器扫描待发事件并批量发送 rows = conn.execute( "SELECT id, event_type, payload FROM outbox WHERE status='pending' LIMIT ?", (batch_size,)).fetchall() published = [] with conn: for row in rows: print("publish:", row[1], row[2]) conn.execute("UPDATE outbox SET status='sent' WHERE id=?", (row[0],)) published.append(row[0]) return published def consume_event(conn, event_id, event_type, payload, seen): # 消费者幂等去重 if event_id in seen: print("duplicate ignored:", event_id) return False seen.add(event_id) print("consume:", event_type, payload) return True def main(): conn = init_db() seen = set() for i in range(5): place_order(conn, "order-" + str(i), 100 + i) published = publish_batch(conn) for pid in published: row = conn.execute( "SELECT event_type, payload FROM outbox WHERE id=?", (pid,)).fetchone() consume_event(conn, pid, row[0], row[1], seen) # 模拟消息队列重复投递 row = conn.execute( "SELECT event_type, payload FROM outbox WHERE id=?", (published[0],)).fetchone() consume_event(conn, published[0], row[0], row[1], seen) if __name__ == "__main__": main()
with conn:利用 SQLite 事务上下文,保证两条 INSERT 要么同时提交,要么同时回滚,这正是 Outbox 的原子性基础。INSERT INTO outbox VALUES (...)在执行业务写入的同时落一条待发事件,事件自带 UUID 作为唯一流水号。SELECT ... WHERE status='pending'发布器只认“待发”状态,避免把已经发过的信再搬一遍。UPDATE outbox SET status='sent'发送成功后立即标记,防止同一事件被反复发布。if event_id in seen消费者用事件 ID 做幂等判断,重复投递的消息会被直接忽略。
常见误区
误区把 outbox 表当成消息队列的数据库版,认为事件存在数据库里就行,不需要再接入 Kafka、RabbitMQ 这类 MQ 系统。
正解Outbox 表只是“待发信箱”,它负责暂存事实并保证与业务数据原子落库,但真正的广播、订阅、削峰填谷还是要靠消息中间件。如果永远不把信从抽屉里取出来,收件人永远收不到通知。
为何常见:看到“事件先写数据库”的字面意思,容易误以为 Outbox 是存储型消息系统,忽略了它后面还得有发布器与 MQ 配合完成完整链路。
误区认为只要引入了 Outbox,消息就一定只被消费一次,消费者可以什么都不做。
正解Outbox 只能保证“事件不丢失”,无法保证“事件不重复”。消息队列通常采用至少一次投递,网络超时、消费者崩溃都会导致重复消息。消费者必须用唯一业务 ID 做幂等,才能真正避免重复处理。
为何常见:把“可靠投递”与“恰一次处理”混为一谈,忽略了消息语义中至少一次与幂等消费是两个不同层次的责任。
误区认为只要坚持不懈地重试,事件迟早会发送成功,所以不需要限制重试次数。
正解无限重试会形成重试风暴,把下游系统和消息中间件压垮。正确做法是采用指数退避并设置最大重试次数,超过上限后转入死信队列或人工告警,保证系统在故障期间仍然可控。
为何常见:出于“不能丢消息”的心理,开发者容易走向另一个极端,忽略重试本身对系统的额外负载。
误区把 Outbox 当成万能药,认为只要加上它,所有服务间的强一致性需求都能满足。
正解Outbox 只解决“一个服务的本地事务与该服务要发出的事件之间的原子性”,它是最终一致性体系的一部分,并不能让多个服务实时强一致。如果业务真的要求跨服务强一致,仍需借助分布式事务中间件或重新设计业务边界。
为何常见:看到 Outbox 在订单场景效果显著,就误以为它能覆盖所有场景,忽略了它的适用边界是“本地事务 + 异步通知”。
误区为了让消息发布更及时,把轮询间隔设得极小,甚至每 10 毫秒扫一次 outbox 表。
正解过于频繁的轮询会放大数据库压力,反而拖慢真正的事务写入。轮询间隔、批量大小与业务允许的最大延迟之间需要做权衡,通常秒级轮询已经足够;对延迟极敏感的场景,应改用 CDC 而不是缩短轮询周期。
为何常见:人们在优化时习惯线性思维,以为“更频繁=更好”,忽略了资源消耗与收益的边际递减效应。
面试题
Q1. Outbox Pattern 的核心目的是什么?
- A.将业务数据同步到消息中间件
- B.业务表与消息表同一事务提交← 正确答案
- C.以本地数据库替代消息中间件
- D.通过内存缓存加速消息查询
答案:B。Outbox 的核心目标是让业务表写入和事件表写入处于同一个本地数据库事务,避免双写不一致。A 是发布器的职责,C 曲解了组件定位,D 与消息可靠发布无关。
Q2. Outbox 表中的事件状态通常包括哪两种?
- A.pending / sent← 正确答案
- B.start / finish
- C.empty / overflow
- D.visible / hidden
答案:A。待发事件用 pending 表示,已发送事件用 sent 表示。发布器只扫描 pending 状态的记录,发送成功后将状态改为 sent。其余选项都不是工程中通用的状态建模。
Q3. 在订单服务和消息队列间使用 Outbox,哪个组件负责扫描并发送事件?
- A.运行业务逻辑的控制器
- B.独立的消息发布器← 正确答案
- C.数据库索引自己触发
- D.前端网关负责转发
答案:B。Outbox 要求独立发布器(publisher)异步读取 outbox 表并发送到 MQ。控制器只负责业务写入,数据库索引不主动发消息,前端网关也不参与内部事件转发。
Q4. 为什么消费者必须做幂等处理?
- A.因为 Outbox 会丢失消息
- B.因为消息队列可能重复投递← 正确答案
- C.因为数据库事务会重复提交
- D.因为消费者需要更高并发
答案:B。消息队列通常提供至少一次投递语义,网络抖动或消费者超时都会触发重投。Outbox 不丢消息但也不消重,因此消费者必须以唯一业务 ID 做幂等,避免重复扣款、重复建单。
Q5. CDC 方案在 Outbox 模式中监听的是什么?
- A.消息队列的消费日志
- B.网络请求的访问日志
- C.数据库的 binlog / WAL← 正确答案
- D.应用服务器的运行日志
答案:C。CDC 通过解析数据库的 binlog 或 WAL 捕获 outbox 表的新增变更,实现准实时的消息发布。它监听的是数据库日志,而 MQ 日志、访问日志和运行日志都不是 CDC 的数据源。
Q6. 关于 Outbox 发布失败的重试策略,推荐做法是?
- A.无限间隔重试直到最终成功
- B.指数退避并设定最大重试次数← 正确答案
- C.每次固定一秒重试且不设上限
- D.失败后直接丢弃并记录错误日志
答案:B。推荐指数退避加最大重试上限,既能给系统恢复时间,又能避免无限重试压垮下游。固定间隔无限重试和直接丢弃都是不可取的极端做法。
Q7. Outbox Pattern 能解决的核心一致性问题是?
- A.服务间强一致性问题
- B.本地事务与跨系统通知的原子性← 正确答案
- C.多个服务共享同一个分布式锁
- D.缓存与数据库的双写一致性
答案:B。Outbox 解决的是“业务写入”与“事件通知”之间的原子性:两者在同一本地事务内完成,从而保证后续异步通知不丢失。它不解决多服务强一致、分布式锁或缓存一致性。
Q8. 当 Outbox 发布器与消费者并行运行时,如何保证事件顺序最终一致?
- A.依赖消息队列单分区内有序← 正确答案
- B.使用全局唯一时间戳排序
- C.每次发布前获取分布式锁
- D.不处理顺序,直接丢弃乱序消息
答案:A。可靠做法是把同一业务聚合的事件路由到 MQ 的同一个分区,利用分区内有序特性保证消费顺序。全局时间戳在分布式环境下难以精确对齐,分布式锁开销过大,直接丢弃乱序消息则会造成数据错误。
Q9. 使用 Outbox 时,消费者的关键保障包括哪些?
- A.为每个事件设计唯一业务 ID← 正确答案
- B.消费前通过唯一 ID 做去重← 正确答案
- C.将重复事件直接写入主数据库
- D.为每个事件创建独立前端页面
答案:AB。唯一业务 ID 是幂等的钥匙,消费前去重用于拦截重复投递。重复事件直接写库会破坏幂等,为事件创建前端页面与消费者职责无关。
Q10. 设计高吞吐 Outbox 系统时,下列哪些做法是有效优化?
- A.按业务维度拆分事件分区← 正确答案
- B.批量读取并使用批量发送← 正确答案
- C.忽略已发送记录的清理工作
- D.监控 pending 积压量并告警← 正确答案
答案:ABD。分区可以降低锁竞争,批量发送能提升吞吐,监控积压能让问题提前暴露。忽略已发送记录清理会导致 outbox 表无限膨胀,查询扫描代价越来越大,因此 C 是负优化。