深夜,灯火通明的“微服务大酒店”生意兴隆。客人入住、点餐、退房,每个动作都牵动着前台、厨房、客房部和财务室。麻烦的是,每个部门都有自己的小账本,客人入住了,前台却忘了通知厨房送果盘;客人退房了,财务却迟迟不知道要结账。过去,大酒店靠“铁索连环”式的大总管(分布式事务)强控全场,谁办事都要等大总管点头,效率极低,一次停电就全楼瘫痪。后来,聪明的经理发明了两件法宝:前台登记薄(Outbox 表)和值夜门童(CDC 组件)。前台每做成一笔生意,先把要点记在登记薄上,门童盯着登记薄,发现新记录立刻抄送到各部门。各部门不必同时办事,但最终都能对得上账——这就是“最终一致性”。“前台记账不怕系统崩溃,门童送信不怕消息丢失”,这套朴素的酒店管理智慧,正是 Outbox 模式与变更数据捕获(CDC)在微服务世界里大放异彩的根本原因。本文将以这家大酒店为缩影,带领零基础的读者看懂分布式系统如何用“一本登记薄 + 一位门童”解决数据一致性与消息可靠投递两大难题。
对 AI 从业者而言,理解 Outbox 与 CDC 等于掌握微服务可靠通信的“心脏搭桥术”:既能设计出不丢消息、不坏数据的系统,也能在生产事故中快速定位“消息到底去哪了”,这是分布式系统工程师的必修内功。
可视化演示
演示Outbox模式事务写入、CDC变更捕获与事件发布的全流程,展示业务数据与事件投递如何实现最终一致性。
本节配有交互动效(进入视口后自动加载,约 3KB)。
写入阶段 业务表与Outbox表同库事务写入 原子性:要么双写成功,要么全部回滚 避免双写问题与事件丢失。
一致性问题
大酒店每个部门都有自己的小本子。前台记下了“客人要餐后甜点”,厨房的账单上却始终没有这一笔——起因就是两个部门的记录动作发生在不同时间、不同地点,中间任何一个环节宕机,信息就断了。微服务架构下的“双写矛盾”正是如此:订单服务既要往订单表插入一行,又要向消息队列发送一条“订单已创建”事件。这两个动作发生在不同的存储系统上,数据库提交成功了,消息发送却可能失败;消息先发出去,数据库事务又可能回滚,造成下游收到凭空的事件。两边总难两全。
传统解法是引入分布式事务大总管(如 2PC/XA):所有参与方预先锁定资源,全部就绪才一起提交。这就像酒店要求厨房、客房、财务同时停下手头工作,举手表决完再继续干活——一次故障就全局阻塞,性能损耗极大,运维复杂,很多团队敬而远之。于是业界转向 BASE 思想:不追求每时每刻一致,而是保证“最终一致”。在任意时刻,各部门的账本可能短暂不同,但经过一段时间后,所有账本都会收敛到相同的事实。
回到酒店:各部门不必在同一秒对账,但打烊前总能对平账目。Outbox 模式与 CDC 组件正是帮助各部门“最终对账”的得力工具。
- 状态函数$S_A(t)$ 表示 A 系统在时刻 $t$ 的账本状态,比如订单库里“订单已创建”这一事实。
- 极限趋同当时间 $t$ 不断增大,$S_A(t)$ 与 $S_B(t)$ 的差距被不断拉近并最终变为相等。
- 收敛结论最终一致性不是“不保证一致”,而是“保证最终收敛”,这为 Outbox 与 CDC 提供了理论空间。
Outbox 模式
经理给前台配了一本专用的“登记薄”。每一笔生意成交时,要求前台在同一支笔、同一张桌面上完成两件事:在客户账单上记一笔,同时在登记薄上誊写一份“待办小纸条”。这两笔记录要么同时写成、要么同时撕掉,绝不会出现只写其一的情况。登记薄上的小纸条,就是 Outbox 表里的待发事件记录。
映射到技术世界,Outbox 模式的核心是:业务操作和事件记录放在同一个数据库事务中。订单服务在开启事务后,先往订单表插入业务数据,再往 outbox 表插入一条事件记录(记录事件 ID、类型、业务 ID、负载和状态),然后一起提交。这样一来,“业务是否成功”与“事件是否应当被发送”就被绑定成了同一个事实。只要业务数据在,Outbox 里对应的那行就必然也在;如果业务回滚,Outbox 那行也一并消失。随后,独立的后台进程(或 CDC 组件)扫描 outbox 表中状态为“待发送”的记录,将其投递到消息队列,投递成功后把状态标记为“已发送”。
一张典型的 outbox 表包含:event_id(事件唯一标识)、biz_id(关联业务主键)、event_type(事件类型)、payload(事件负载 JSON)、status(待发送/已发送)、retry_count(重试次数)、created_at(创建时间)。核心不变量可以用公式表达:
登记薄模式的优势在于:把分布式难题转化成单机原子写问题。前台只管记好账,送信的事交给门童,业务系统不再与消息队列直接耦合。
- 事务提交$\text{Commit}(\text{Tx})$ 表示数据库事务整体提交成功,数据落盘生效。
- 业务写入$W_{biz}$ 是业务表数据写入成功,比如订单行落库。
- 登记写入$W_{outbox}$ 是 Outbox 表写入成功,事件“待发”状态落库。
- 同生共死$\wedge$ 表示两者必须同时为真,事务才提交——这是整个模式的根基。
事务原子边界
为什么“在同一张桌上用同一支笔”如此重要?因为数据库的本地事务天然具备 ACID 特性——原子性(Atomicity)保证事务里的所有操作要么全部执行、要么全部不执行。酒店账本是钉死的,每一页都不能单独撕掉;只要前台在写客户账单时笔迹没干,登记薄上的小纸条也等于不存在。
在 Outbox 模式中,业务写入与 Outbox 写入处在同一个本地事务里,这个事务的边界就是一致性边界。假设系统在写完订单表后、写 Outbox 前崩溃:恭喜,整个事务回滚,订单表也看不到那条订单,不会出现“业务已存在但事件缺失”的孤儿状态。反过来,如果事务提交成功,Outbox 记录也就一起成功落盘——即使进程随后崩溃,重启后依然能扫描到那条“待发送”记录,事件不会凭空蒸发。这个方案的绝妙之处在于它不需要分布式事务协议,不锁远端资源,只依赖普通数据库本就具备的事务能力。
前台可以放心大胆地记:要么两笔都成,要么两笔都撕。事务边界像一道坚实的防火墙,把“不一致”扼杀在入口处。
- 事务集合$\text{Tx}$ 代表一个数据库事务,花括号中包含本次要执行的两个写操作。
- 全有或全无$\text{all or nothing}$ 是原子性的数学表达,事务结果只有两种:全部提交或全部回滚。
- 无中间态外部观察者永远看不到“订单写入成功而 Outbox 写入失败”的中间画面。
CDC 采集原理
登记薄记好了,谁来发现并传递这些“待办小纸条”?靠前台每隔几分钟翻一次本子(定时轮询)也可以,但既打扰生意又滞后。聪明的经理雇了一位门童:他不需要客人打招呼,只站在登记薄旁边,眼观六路,看到新出现的小纸条,立刻抄录并送往对应部门。更妙的是,门童不是主动去翻本子,而是“监听”本子的翻页声——他盯的是数据库的日志文件(binlog / WAL),这就是变更数据捕获(CDC)。
CDC 的核心思想是利用数据库主从复制同款机制读取事务日志。以 MySQL 为例,开启 binlog 并设置为 ROW 格式后,每次数据变更都会产生一条包含行级前后镜像的日志记录。CDC 组件(如 Debezium)伪装成从库,持续读取 binlog,解析出“哪一行在何时被插入/更新/删除”,再把行变更转换成结构化的消息事件,推送到消息队列。相比应用层轮询,CDC 有三大优势:延迟低(日志产生即读取,毫秒级发现)、侵入低(业务代码无须改动,不额外写更新时间戳字段)、可靠性高(日志是数据库持久化的,不会因业务进程崩溃丢失)。
门童的耳朵贴着账本,纸一响他就出发。CDC 正是这样一位反应敏捷、从不打扰客人的值班门童。
- 提交时刻$t_{commit}$ 是业务事务提交、数据在数据库中可见的时刻,一切延迟从此刻算起。
- 扫描耗时$t_{scan}$ 是从日志中识别出该变更所需的时间,CDC 让它从秒级降到毫秒级。
- 投递耗时$t_{publish}$ 是消息写入 MQ 并被消费者拉取的网络与缓冲耗时。
- 消费耗时$t_{consume}$ 是消费者执行业务逻辑与幂等判断的耗时。
组件选型集成
选门童要看眼力和腿脚,也要看他和各部门信使的配合默契。当前主流方案是 Debezium 加 Kafka:Debezium 是“金牌门童”,支持 MySQL、PostgreSQL、Oracle 等多种数据库,把 binlog 解析成 Kafka 事件流;Kafka 则是高效的“传菜通道”,负责把事件分发给下游各业务系统。另一条热门路线是 Flink CDC:它在流计算生态中如鱼得水,适合需要实时数仓同步、复杂事件处理的场景。两个方案都能与 Outbox 表无缝衔接——门童读到登记薄的新行,直接转成消息进 MQ。
集成时有一个关键步骤:快照与增量的衔接。系统首次启动时,CDC 组件需要先扫描整个表做一次全量快照,从快照完成的位置记录 binlog offset,再无缝切换到增量监听。用公式表达选型决策,就是给各候选方案按权重打分:
门童队伍的配置不是一劳永逸的。先小范围试运行,观察延迟与资源占用,再逐步扩展到核心链路,是稳妥的酒店改革路径。
- 候选维度$i=1$ 到 $n$ 表示评价维度集合,例如延迟、吞吐、运维成本、社区活跃度。
- 可配权重$w_i$ 是每个维度的权值,例如交易系统更看重延迟,数仓同步更看重吞吐。
- 加权求和$S$ 是总分,分数最高者即当前上下文的最优选型。
可靠投递幂等
门童把信送到各部门,但邮路并不完美。消息队列(如 Kafka、RocketMQ)走的是“至少一次”(at-least-once)语义:为了不丢消息,宁可你多收几遍,也绝不让你少收一遍。于是客人可能收到两封一模一样的致歉信——消费者必须练就“同一封信只处理一次”的本领,这就是幂等性。数学上说,对一个状态执行一次操作和执行多次操作,结果完全一样:
实现幂等的具体手段包括:数据库对事件 ID 建唯一索引,重复插入直接冲突跳过;或维护一张已处理事件表,消费前先查重;也可用业务状态机判断——只有“待处理”状态才能流转到“处理中”,已经处理过的直接忽略。顺序问题上,让同一业务主键的事件进入同一个 MQ 分区即可保证同一订单的事件严格有序;若依然产生乱序,可在事件负载中加入单调递增的序号,消费者按序号校验。
- 一次处理$f(x)$ 表示消费者对消息 $x$ 执行一次业务处理,比如入账 100 元。
- 再次处理$f(f(x))$ 表示同一事件被重复投递后再次执行处理。
- 结果不变两次处理的状态与一次处理完全相同,重复的消息被无声吸收。
监控与优化
门童再勤快,也有送信高峰和暴雨停电。酒店必须实时盯住两个指标:门童从发现纸条到送达的延迟(CDC 端到端延迟),以及传菜通道上的积压量(MQ 消费者 lag)。延迟上涨,说明下游处理能力不足;积压暴涨,说明消费者可能出现故障。此时盲目重试只会让系统雪上加霜——正确做法是指数退避:第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒,最多退避到设定的上限。
Outbox 表也不能无限膨胀。已发送且超过保留期的记录应定期归档或删除;可以按创建时间分区,删除时直接 drop 分区,效率远高于逐行 DELETE。门童的断点续传能力则依赖持久化记录 binlog 消费位点,进程重启后从上次位置继续监听,确保不重不漏。
- 初始等待$t_0$ 是第一次重试前的基准等待时间,比如 1 秒。
- 指数增长$2^n$ 让等待时间按指数膨胀,避免失败后的密集重试压垮下游。
- 封顶保护$T_{\max}$ 是等待时间的上限,防止无限等待导致事件迟迟无法投递。
工程实战排查
当客人大喊“我定了房间但厨房没收到订单”,酒店应当如何排查?答案是沿着登记链条逐环检查,绝不瞎猜。第一环:查订单表,业务行是否真的存在——不存在说明事务压根没成功,问题在业务逻辑;第二环:查 Outbox 表,对应记录是否在、状态是否为“待发送”——不存在说明事务边界被破坏,存在则说明写入正常;第三环:查门童日志和 MQ 消费位点——事件是否已发布、消费者是否已拉取;第四环:查消费者日志——是否执行了业务逻辑、幂等判断是否误伤。这套流程就像顺着水管找漏水点,一环都不放过。
性能层面,需要建立基准线:记录 CDC 从 binlog 读取到 MQ 落盘的延迟分布,关注 p99 而非平均值;对 Outbox 表的写入和清理做压测,估算单表支撑的最大吞吐。可用 SLO 表达可靠性目标:
酒店把一次停电事故的复盘写成标准手册,之后每位新员工都知道先看登记薄再问门童。Outbox 与 CDC 的工程实践同样如此——把排查经验沉淀为流程,把可靠性量化为指标,最终让复杂分布式系统的日常维护变得井井有条。
- 延迟目标$t_{e2e}$ 是事件从提交到消费完成的全程耗时,$\tau$ 是业务可接受的阈值(如 1 秒)。
- 概率约束$P(\dots)$ 表示满足条件的时间占比,99.5% 意味着 1000 条事件中至多 5 条超时。
- 持续守护把该指标接上监控告警,一旦连续多个窗口不达标,立即触发值班排查。
AI 应用场景
订单履约链路的事件可靠同步
阶段:数据同步与事件驱动集成
解决的问题:订单服务需要在创建订单后通知库存、积分、物流等多个下游服务。传统做法是业务代码里依次调 MQ 发送接口,一旦发送失败需要补偿逻辑,且数据库与 MQ 之间的一致性无法保证。
具体案例:某电商平台订单服务使用 MySQL 存储订单,引入 Outbox 表 + Debezium + Kafka。用户在 App 下单时,订单与 Outbox 记录在同一事务中落库;Debezium 监听 binlog 将订单事件推送到 Kafka;库存、积分、物流各自消费并幂等处理。上线后订单事件丢失率为零,数据库与下游系统的数据最终一致。
实时数仓与数据湖入湖
阶段:数据采集
解决的问题:将业务库数据实时同步到数仓,以往用定时批处理(T+1)延迟高,用应用层双写又侵入业务代码。CDC 直接从日志采集变更,对源库零侵入。
具体案例:某金融公司采用 Flink CDC 将订单库、用户库的增量变更实时写入数据湖 Iceberg,配合 AI 风控模型实时计算用户行为特征。整个链路从数据库提交到数仓可见的延迟控制在秒级,且支持全量快照与增量无缝衔接。
支付对账系统的异常追查
阶段:监控排查
解决的问题:支付成功后下游通知丢失,容易引发客诉。通过 Outbox + CDC 的事件链路,支付记录与支付成功事件在同事务落库,任何一环缺失都能依据 outbox 状态与 MQ 消费位点快速定位。
具体案例:某支付公司在排查一笔“已扣款但未通知商户”的投诉时,按照订单表→outbox 表→CDC 日志→MQ 消费位点→消费者日志的顺序逐步定位,最终发现是消费者幂等表误判导致事件被跳过,修复后同类故障清零。
Python 演示
import sqlite3import jsonimport timeimport random class OutboxPatternDemo: def __init__(self): self.conn = sqlite3.connect(":memory:") self._init_schema() self.sent_count = 0 self.processed_orders = set() def _init_schema(self): c = self.conn.cursor() c.execute( "CREATE TABLE orders (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "item TEXT NOT NULL, amount INTEGER NOT NULL)" ) c.execute( "CREATE TABLE outbox (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "biz_id INTEGER NOT NULL, " "event_type TEXT NOT NULL, " "payload TEXT NOT NULL, " "status TEXT DEFAULT 'pending', " "retry_count INTEGER DEFAULT 0)" ) self.conn.commit() def create_order(self, item, amount): with self.conn: cur = self.conn.cursor() cur.execute( "INSERT INTO orders (item, amount) VALUES (?, ?)", (item, amount), ) oid = cur.lastrowid payload = json.dumps( {"order_id": oid, "item": item, "amount": amount} ) cur.execute( "INSERT INTO outbox (biz_id, event_type, payload) " "VALUES (?, 'ORDER_CREATED', ?)", (oid, payload), ) return oid def poll_outbox(self): cur = self.conn.cursor() cur.execute( "SELECT id, biz_id, payload FROM outbox " "WHERE status = 'pending' LIMIT 3" ) return cur.fetchall() def publish_event(self, event): if random.random() < 0.3: return False eid = event[0] with self.conn: self.conn.execute( "UPDATE outbox SET status = 'sent' WHERE id = ?", (eid,) ) self.sent_count += 1 return True def consume_event(self, payload_text): data = json.loads(payload_text) order_id = data["order_id"] if order_id in self.processed_orders: print(f"[消费者] 订单 {order_id} 已处理过,幂等跳过") return self.processed_orders.add(order_id) print( f"[消费者] 处理订单 {order_id}: " f"商品 {data['item']}, 金额 {data['amount']}" ) def main(): svc = OutboxPatternDemo() svc.create_order("机械键盘", 599) svc.create_order("降噪耳机", 1299) for _ in range(5): events = svc.poll_outbox() if not events: print("[扫描] Outbox 无待发送记录") break for event in events: if svc.publish_event(event): svc.consume_event(event[2]) else: print(f"[投递] outbox_id={event[0]} 失败,稍后重试") time.sleep(0.2) print(f"\n已投递事件总数: {svc.sent_count}") if __name__ == "__main__": main()
with self.conn:利用 sqlite3 上下文管理器开启事务,保证INSERT业务表和INSERToutbox 表要么同时成功要么同时回滚。INSERT INTO outbox (biz_id, event_type, payload)将事件负载以 JSON 形式写入 Outbox 表,状态默认pending。SELECT ... FROM outbox WHERE status = 'pending' LIMIT 3模拟后台调度器扫描待发送事件,每次批量取 3 条。random.random() < 0.3模拟消息队列投递时有 30% 概率失败,用以观察重试行为。UPDATE outbox SET status = 'sent'投递成功后更新状态,防止下次扫描重复发送同一事件。if order_id in self.processed_orders消费者幂等判断:同一订单重复到达时直接跳过。
常见误区
误区刚开始学习时以为既然 Outbox 表记录了待发事件,那让下游服务直接轮询 Outbox 表就行,根本不需要 Kafka、RocketMQ 这类消息队列。
正解Outbox 表的职责是“可靠地记录事件待发状态”,它不负责事件的路由、缓冲、削峰和广播。下游服务如果直接查 Outbox 表,一方面会对业务库产生额外读压力,另一方面无法实现多个订阅者解耦消费。门童把信抄录到登记薄,不等于信已送到客人手中;MQ 才是真正的传菜通道,Outbox 保证“不漏单”,MQ 保证“高效送达”。
为何常见:从代码结构看,Outbox 表和 MQ 都能让事件“持久化”,容易混淆存储与传输两种不同职责。
误区认为只要最终能把两行数据都写进去就行,于是先提交订单事务,再单独执行一条 INSERT 写 Outbox 表,觉得失败了补插即可。
正解两个独立事务之间存在时间窗口:订单已提交但 Outbox 还没写,此时进程崩溃,事件永远不会被发送,消费者永远不知道新订单存在。这正是 Outbox 模式最想消灭的“孤儿状态”。正确做法是把两个 INSERT 放进同一个本地事务,利用行锁与日志原子性让它们同生共死——前台记账和誊写登记薄必须用同一支笔在同一秒完成。
为何常见:习惯了普通服务里“先做核心业务再做副作用”的编码方式,没有意识到这里的两步操作跨越了一致性边界。
误区以为引入 Outbox 模式后,事件从业务库到消费者的全程就万无一失,下游不需要做任何额外保障。
正解Outbox 只保证事件“一定会被尝试投递”,但投递过程遵循消息队列 at-least-once 语义,可能重复;消费者宕机、MQ 集群故障也可能导致事件处理中断。可靠的端到端投递需要三件事共同配合:Outbox 记录不丢、投递有重试、消费者幂等去重。三者缺一,链路都不算完整。
为何常见:把“消息不会丢失”误解为端到端的单点保证,忽略了下游处理阶段的多样故障模式。
误区认为使用 binlog 监听后,数据库一提交,下游立刻就能看到事件,而且完全不影响数据库性能。
正解CDC 确实把延迟从分钟级降到秒级甚至毫秒级,但依然存在日志解析、网络传输、MQ 缓冲等开销;t_{e2e} 由提交、扫描、投递、消费四段构成。同时 CDC 连接器是常驻进程,会消耗一定的 CPU 与内存资源,高峰期的频繁 DDL 也可能造成解析延迟抖动。衡量系统不能只看“有没有用 CDC”,还要看 p99 延迟与资源占用。
为何常见:厂商宣传与教程往往突出“毫秒级”这类关键词,掩盖了端到端链路中除扫描之外的其余耗时。
误区以为事件进入 Kafka 这类 MQ 后天然全局有序,消费者收到的事件顺序必然与数据库提交顺序一致。
正解MQ 只能保证同一分区(Partition)内消息有序,全局多个分区之间的相对顺序并不保证。要让同一订单的事件有序,必须用业务主键(如订单 ID)作分区键,让该订单的所有事件进入同一分区;同时 Outbox 记录的自增 ID 可作为数据库提交顺序的依据。乱序场景下,消费者还要结合事件序号判断是否处理迟到的旧事件。
为何常见:把消息队列的“队列”字面理解为单一切口,忽略了分布式 MQ 通过多分区提升吞吐的本质设计。
面试题
Q1. Outbox 模式最核心解决的问题是什么?
- A.提升数据库查询性能
- B.业务数据与消息发送的一致性← 正确答案
- C.解决服务间接口调用超时
- D.替代消息队列的高吞吐传输
答案:B。Outbox 模式把业务写库与事件落表放进同一事务,解决“库成功而消息未发”的原子性矛盾。它不提升查询性能,也不替代 MQ 的传输能力;接口超时是网络层问题,与数据一致性无关。
Q2. 在 Outbox 模式中,业务表写入与 Outbox 表写入应当满足什么关系?
- A.任意先后,失败可补
- B.先写 Outbox 再写业务表
- C.同一数据库事务中原子完成← 正确答案
- D.分别由两个独立事务完成
答案:C。两个写入必须处于同一本地事务,要么同时提交要么同时回滚。分两个事务会留下时间窗口,进程崩溃即可导致业务存在而事件缺失的孤儿状态。
Q3. CDC(变更数据捕获)主要读取以下哪类数据来发现变更?
- A.应用日志文件
- B.数据库事务日志(binlog/WAL)← 正确答案
- C.Redis 缓存中的热点数据
- D.消息队列的消费位点
答案:B。CDC 利用主从复制同款机制读取数据库事务日志,从中解析出行级变更事件。应用日志不含行级数据变更;Redis 与消费位点不是 CDC 的数据来源。
Q4. 消息队列“至少一次”语义对消费者的要求是?
- A.消费者必须保证事务性回滚
- B.消费者必须做幂等去重处理← 正确答案
- C.消费者必须提升消费吞吐
- D.消费者必须增加消息缓冲
答案:B。at-least-once 意味着消息可能重复投递,消费者要能识别重复并保证多次处理的副作用与一次相同。回滚、吞吐、缓冲都不能消除重复带来的影响。
Q5. 数据库本地事务的原子性在 Outbox 模式中的作用是?
- A.保证两次写入性能最优
- B.保证业务写入与事件写入同生共死← 正确答案
- C.保证消息队列消息不丢失
- D.保证消费者处理顺序一致
答案:B。事务的 all-or-nothing 语义让业务表与 Outbox 表要么一起落库、要么一起消失,这是 Outbox 可靠性的基石。消息队列丢消息与消费者顺序是链路下游问题,不属于本地事务管辖。
Q6. 下列关于 Outbox 表设计的说法正确的是?
- A.Outbox 表不需要状态字段
- B.payload 无需保存具体事件数据
- C.事件 ID 是为了幂等和追溯← 正确答案
- D.Outbox 表数据永不清理
答案:C。事件 ID 是唯一标识,消费者可用它做去重,运维用它做链路追踪。状态字段记录待发/已发,payload 承载事件数据,已发送记录需按保留期归档清理。
Q7. 某订单事件被重复投递到消费者,优化做法是?
- A.每次更新库存前先查询一遍
- B.用订单 ID 做下次投递去重判断← 正确答案
- C.直接丢弃所有重复消息
- D.让生产者发送前先等待确认
答案:B。正确做法是基于事件 ID 或业务 ID 建唯一索引,重复消息在幂等判断时直接跳过。查询后再更新不是原子操作,仍可能重复;直接丢弃重复消息可能丢弃的是业务含义不同的合法消息;等待确认不是消费端幂等方案。
Q8. 同一业务主键的消息进入同一 MQ 分区,主要保障了什么?
- A.全局所有消息严格有序
- B.该业务主键下的事件顺序一致← 正确答案
- C.消费者的高可用容灾
- D.消息不丢失的持久化能力
答案:B。MQ 只保证分区内有序。同一个业务主键(如同一订单 ID)路由到同一分区后,该事件流的顺序得到保证,而不同分区之间依然无全局顺序。高可用与持久化是 MQ 的其他能力。
Q9. 使用 Debezium 与 Kafka 集成 Outbox 模式时,需要关注哪些环节?
- A.binlog 是否开启并设置为 ROW 格式← 正确答案
- B.Outbox 表结构变更时的 DDL 处理← 正确答案
- C.Kafka 消费位点积压的监控告警← 正确答案
- D.用 MySQL 索引替代 Kafka 做消息路由
答案:ABC。开启 binlog 并设为 ROW 是 Debezium 工作的前提;DDL 变更会引起解析链路波动,必须纳入治理;消费位点积压反映下游处理能力,需要监控。Kafka 做路由与缓冲,MySQL 索引不能替代。
Q10. 某系统 Outbox 表中堆积大量“待发送”记录且重试均失败,排查步骤包括哪些?
- A.检查消费者服务是否宕机或异常← 正确答案
- B.确认 MQ 集群是否处于可用状态← 正确答案
- C.人为手工将状态改为“已发送”
- D.查看 CDC 消费位点与异常日志← 正确答案
答案:ABD。投递失败应从消费者、MQ、CDC 链路逐环排查,确认事件有没有进入 MQ、消费者有没有成功处理。手工改状态会掩盖问题导致数据永久丢失,是绝对禁止的操作。