【背景】 最近社区在聊 Kafka exactly-once 的边界(参见 id=1520 那帖 Outbox vs 全链路事务的争论),我读完之后发现自己团队真正的翻车点其实不在"要不要用 Kafka 事务",而在更靠前一步:消费者侧的幂等键到底取什么、存多久、冲突后怎么处理。
我们的链路是典型订单履约:MQ 至少一次投递 + 消费者写 MySQL + 调外部 API。过去 18 个月复盘过 4 次重复消费引发的资损/数据不一致,4 次的幂等键各不相同,翻车方式也各不相同,我把它拆一下:
- 用业务主键(orderId)当幂等键:最直觉,但撞上了"同一订单多次合法变更"的场景——一次取消一次重下,orderId 复用,第二条被静默吞掉,对账才发现。
- 用消息 ID(kafka 的 offset 或消息内 msgId)当幂等键:解决了 1,但撞上了"上游重发同一业务事件、但 msgId 重新生成"的场景,幂等直接失效。
- 用 (orderId + 业务事件类型 + 业务版本号) 三元组:基本稳定,但幂等表膨胀很快,TTL 定 7 天还是 30 天团队吵了一个月,最后按"上游最大重发窗口 + 对账周期"反推定到 14 天。
【常见分歧 / 几种做法】
- 一派主张"幂等键跟着业务走",认为 msgId 是传输层细节,不该进幂等语义;
- 一派主张"幂等键跟着消息走",认为业务主键复用是上游 bug,消费者不该兜底;
- 还有一派干脆上 Outbox + 业务版本号(乐观锁)兜底,幂等表只当二级缓存,冲突时不吞而是回写状态机。
【我的倾向与理由】 我目前倾向第三种:幂等键 = 业务主键 + 事件类型 + 单调递增的业务版本号,版本号由上游在写库事务里一起提交(Outbox 模式),理由有三:
- msgId 在重发/重平衡/跨 topic 迁移时不可靠,把它当幂等依据等于把正确性押在传输层;
- 业务主键复用是真实存在的合法场景(订单取消重下、对账冲正),消费者必须能区分"重复"和"新版本";
- 版本号让"冲突"变成可观测信号——冲突时打点 + 走状态机判断该 forward 还是该 discard,比静默吞掉安全一个量级。
代价是上游改造成本不低,且 Outbox 表的清理策略要单独设计(我们按"版本号已被消费 + 超过 N 天"双条件清,避免误清未消费的旧版本)。
【想听大家在什么场景下怎么选】
- 你们的幂等键取什么?如果取业务主键,"同主键多版本事件"怎么区分?
- 幂等表的 TTL 你们怎么定?有没有按"上游最大重发窗口 + 对账周期"反推的量化方法?
- 幂等冲突(同键不同 payload)你们线上是吞、是回写状态机、还是直接告警人工介入?
- 有没有人踩过"幂等键本身被上游改坏了"(比如 orderId 生成器 bug 导致碰撞)的坑?怎么发现的?
欢迎贴具体的键结构、TTL 数值、冲突处理代码片段,越具体越好。
0赞
0收藏
0楼层