![]()
能改订单事务,优先把事件一起落库;用七问对比表,选清发布通道、幂等策略与补偿责任。
订单已经落库,履约却没启动:你会补一个发送重试,还是把事件写进同一笔事务?能改订单写入逻辑、暂时没有成熟CDC平台时,我选事务发件箱,先把订单和待发事件一起保存。后台可以晚一点发,发送机会要先留住。
假设订单O-1042已支付,履约库却查不到任务。此时就给消息队列扩容,很可能找错了地方:事件可能还在订单进程的内存里,也可能早已送达,只是业务执行失败。
![]()
▲ 订单已落库,不代表发送机会也被保存。
先分清未发送与未确认,再决定补哪一段
普通消息放在数据库提交前发送,订单回滚后,下游可能已经开始履约。挪到提交后发送,又留下“订单已提交、进程尚未发送就退出”的空窗。事务提交后的内存回调,同样会随进程消失。
沿执行顺序看,发送调用之前退出,查的是持久化待发记录;发送调用之后、生产端确认之前超时,查的是未知结果。消息服务器可能已经收下消息,只是确认包没回来。此时沿用同一个event_id重试,给下游识别重复的机会。
收到生产端确认后,履约仍可能没有完成。先查消费者积压和业务失败;如果消费事务已经提交,只差消费确认没送回,下一次投递会再次执行入口逻辑。
例如,在测试里让发布器收到确认后立即退出。数据库留下1条“待发送”,消息服务器却已有1条事件。重启后同ID再发1次,履约任务应仍只有1条。这个断点能直接检验“未标记”是否被误当成“未投递”。
生产端确认只证明发送走到了哪一步,业务完成要看下游事务里的结果。确认所覆盖的耐久性,还取决于消息服务器的复制与刷盘配置。排障时把订单号、事件ID、履约结果关联起来,比单看“发送成功”日志有效。
![]()
▲ 未发送、未确认与未处理,要查的是三种不同的证据。
原子性保住的是恢复依据,三条路线各留一份
这里比较同一个需求:订单提交后,履约异步完成。比较原子性边界、恢复依据和运行责任。三条路线都以至少一次交付为设计目标,前提是恢复依据仍在、重试能够继续;跨组件业务结果由幂等与对账保障。
轮询事务发件箱,就是把订单更新和outbox事件行放进同一个数据库事务。原子性到这里为止。提交后,发布器再领取事件、发送、确认、标记;标记失败,就继续重发。
租约领取后,在短事务之外发送。例如测试设30秒租约、5秒发送超时,再把工作线程暂停40秒,让另一实例接管。旧线程恢复后,更新条件带上事件ID和本次领取令牌,匹配当前持有者才改标记;重复发送由消费幂等吸收。
CDC是变更数据捕获:从数据库日志里取出已提交变化。它与发件箱可以组合。直接订阅订单表,应用团队要把“行发生变化”解释成“哪件业务发生了”;订阅outbox,事件意图在提交时就已明确。
能改业务事务时,先保存明确的事件意图,再选轮询还是CDC发布。Debezium 3.3的Outbox Event Router默认把事件id放在消息头,aggregateid作消息键。转发层要保留这个头;订单号作键负责路由,发布端另保同订单顺序。
事务消息则把恢复依据分放两处。RocketMQ 5.x先确认半消息,再执行本地事务,随后提交或回滚消息状态。二次确认丢失时,服务端向生产者回查本地结果。半消息是暂时对消费者不可见的消息。
回查应读取与业务操作同事务保存的结果,别拿可能延迟的只读副本作裁决。事务仍在执行或结果尚未查清时返回UNKNOWN。Topic须配置事务类型,Broker与SDK组合要先验证;半消息发送也因此成为订单写入的前置依赖。
恢复有保留期限,少写代码也会增加值班工作
轮询把运行负担留在应用侧。盯住最老待发事件的年龄,比只看记录总数更能发现“一个订单卡很久”。租约到期要能重新领取,坏事件用退避和隔离处理,避免占满发布线程。
CDC把扫描工作交给连接器,也引入位点和日志保留责任。Debezium 3.3的MySQL连接器依赖binlog,并保存表结构历史。停机后如果所需日志已被清理,重新快照拿到的是当前状态,历史业务事件要靠事件归档或业务审计补齐。
拿一个假设恢复预算作比较:binlog按72小时留存,连接器停读24小时才告警,修复又花60小时,期间持续写入且未续读。七问表列出预算缺口。优先延长留存或缩短发现、修复时间,再给追平积压留余量。
事务消息的运行重点是回查与超时。RocketMQ 5.x存在事务超时回滚机制;本地已成功、消息最终未提交的异常,需要进入业务对账。把UNKNOWN积压、回查失败和超时后的修复责任写进值班规则,具体阈值按部署版本确认。
恢复要逐段找仍可用的凭据。binlog清理后,已持久化到消息队列的事件仍可消费;队列过期后,可从事件归档补发。去重记录过期则依靠业务唯一键防重复。事件与归档都缺失时,再按业务事实生成补偿任务。
用业务唯一键兜住回放,再拿七问比较方案
重试恢复的是执行机会。事件ID识别的是同一次事件,业务幂等守住的是同一业务动作。三者分开设计,重试次数才有增加的余地。
对演示订单,给支付事件固定ID“E-1042-1”。消费者以“消费者名称+事件ID”建唯一约束,将成功登记与履约任务创建放进同一事务,提交后确认消费。仅事件去重约束冲突时,回滚当前事务并查已提交结果后确认;其他冲突按业务失败处理。
例如,订单拆成2个履约批次,批次A的事件重放3次,批次B正常到达。应保留2条任务:唯一键取“订单号+批次”,只用订单号会误挡批次B。事件去重记录过期后,也按这个业务键核验。
乱序检测用连续业务事件序号,缺口先暂存或补读;若用订单行版本号,要先排除未产生事件的更新。
重复消息靠事件ID识别,重复履约靠业务唯一键和状态机守住。调外部配送接口时,还要传稳定的业务幂等键。调用超时先查执行结果;对方缺少幂等和查询能力,就保留人工核对出口。
下面七问用于同一订单异步履约场景。每列都按“可能重复、需要恢复”评审;CDC列优先采用outbox+CDC。把空白责任人补齐,再决定选哪条路线。
评审七问
轮询事务发件箱
CDC发布
事务消息:RocketMQ 5.x
1. 原子性到哪,恢复凭什么?
订单与outbox同事务;应用负责
outbox同事务;平台续读日志位点
本地结果回查;半消息协调提交
2. 谁盯积压与异常?
应用盯最老待发、失败、租约
平台盯位点延迟、连接器失败
平台盯回查、UNKNOWN及超时
3. 留存能覆盖故障恢复吗?
待发保留至确认,归档可追溯
例:停读24+60=84小时;超72小时12小时
平台核对半消息超时及消息保留
4. 重试是否沿用ID,谁去重?
原event_id;消费业务唯一键
原事件ID;消费业务唯一键
稳定业务事件ID;消费业务唯一键
5. 谁限速回放、兼容旧事件?
应用按订单、时间筛选补发
平台重放;应用处理版本与乱序
平台重投;应用处理版本与乱序
6. 谁对账,差异按什么算?
业务核对应履约订单与任务
业务核对应履约订单与任务
业务核对应履约订单与任务
7. 谁批准补偿,如何验收?
业务审批;应用修复并留审计
业务审批;平台与应用协作
业务审批;应用修复超时差异
对账口径要排除取消、退款等合法状态,并给正常异步延迟留出宽限。补偿后复核业务差异和重复执行,别拿订单行数减消息条数当作漏单数。外部动作也要有完成证据。
能改事务先用发件箱,有成熟平台再换通道
应用能改、还没有CDC维护团队,我选轮询发件箱。先补消费幂等和对账,再将订单与事件一起写入,挑一条支付后履约流程上线。多一张本地表,比多一套无人维护的日志平台更容易控制故障域。
已有成熟CDC平台、多个下游需要复用事件,选outbox+CDC。确实改不了应用,且表变化足以表达业务事件时,再用业务表CDC加适配层,同时接下DDL耦合与历史缺口补偿工作。
已有RocketMQ事务消息体系,回查服务也有人值守,可以沿用事务消息。把半消息失败纳入订单入口的失败处理。只为省一张表而迁移整套消息基础设施,收益很难覆盖并存期的复杂度。
从轮询切到CDC时,按订单分片或明确水位移交发布责任。影子通道只比对,双发保持相同事件ID。CDC发布通常不更新轮询标记;回退须从留存的outbox按移交水位限速重放,消费去重覆盖并存期。
例如,测试准备100笔已支付、未取消且各需1条任务的订单。分别在提交后发送前、发送成功标记前、消费提交确认前退出进程;每轮重置数据并恢复运行,验收都应得到100条任务、0笔遗漏、0笔重复履约。积压清零也要查业务账。
![]()
▲ 从轮询切到 CDC,要移交发布责任,也要保留可回退的事件依据。
这条订单流程要不要新上CDC,哪个维护约束会让你放弃?
IT架构师联盟
面向企业架构师与技术负责人的专业号:讲企业 IT 架构的选型与演进,把关键决策的取舍和代价讲清楚。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.