已采纳
回答正文
回复
讨论正文
该问题的正文、回答与讨论。
订单 → 履约 → 第三方物流这条链路,Kafka 3.6(3 broker),topic order-event 32 分区按 order_id 哈希,at-least-once + 幂等表。三层幂等键各自独立:
t_outbox,业务键 biz_key = tenant_id|order_id|event_type|event_version(唯一索引),业务事件写 Outbox → Kafka 生产端扫描投递 → 投递成功标记 SENT。messageId 用 UUID.randomUUID(),在 retry 循环里每次重新生成。生产端用 transactional producer(transaction.timeout.ms=60s),"写 Outbox + 投 Kafka"在一个 Kafka 事务里。t_consumed_message,唯一键 uk_message_id。处理流程:解析 → 幂等表查重 → 写本地 t_fulfillment → 调外部物流 API(requestId = biz_key)→ 提交 offset。requestId,TTL 24h,超时 ≠ 失败(可能已成功也可能未成功)。Kafka 事务 abort 后 retry,messageId 漂移:事务超时 / broker 抖动导致 abort,catch 后 retry,retry 时 messageId 重新 UUID.randomUUID() → 两条消息 messageId 不同、biz_key 相同 → 消费端 uk_message_id 全部放行 → 本地 t_fulfillment 被写两次 → 外部 API 用 biz_key 只执行 1 次。结果:本地状态多写、外部只执行 1 次,两边对不上,对账脚本天天报警。
Outbox 表"删除后重插"(数据修复脚本误操作):biz_key 相同、messageId 不同 → 同上问题。但这种情况业务上希望重新消费(因为第一次消费可能已经失败),改成按 biz_key 幂等又会挡住合法的重新投递。
消费端「写本地 → 调外部」的中间态:写本地成功 → 调外部 API 超时(不确定是否成功)→ 消费者重试时:
核心矛盾:本地状态和外部状态没有事务边界,任何一层失败都会留下"本地已写、外部未写"或"本地未写、外部已写"的中间态,且无法从中间态反推真实状态。
biz_key + 序号 派生(而不是 UUID):挡住了 retry 漂移,但 Outbox 删除后重插时 messageId 又相同 → 消费端误拦截合法重发。transactional.id + sequence 做幂等键?有没有不依赖 messageId 稳定性的方案?社区关于 at-least-once + 幂等的讨论不少,但都是"消费端幂等够不够"的层面,没有专门讨论"三层键不一致时谁兜底"的问题:
我这个问题和它们的角度不同:不是"要不要幂等",而是"幂等键有三层、每层都可能失效、权威层级怎么定"。