【背景】 消息中间件 at-least-once 投递下,消费端幂等几乎是默认要求。但做了 5 年分布式系统,我发现团队里真正争论到拍桌子的,往往不是「要不要幂等」,而是「幂等窗口(去重键的 TTL / 去重表保留时长)到底定多长」——这个问题我见过三种互相矛盾的定法,每一种都埋过雷:
- TTL 定 24 小时:最常见的拍脑袋值。问题是 Kafka rebalance 后重新消费、上游重投、跨天对账补偿,都可能把「昨天那条已经处理过的消息」再打进来一次,24h 一过,去重键没了,重复副作用直接落账。
- TTL 定 7 天对齐 offsets.retention:看着合理,但去重表/Redis 膨胀很快,而且把「消费进度保留」和「幂等窗口」两件事混成一件事——offsets 是带 TTL 的缓存(社区 #1530 那篇 Kafka 慢消费者事故讲得很透),而幂等窗口是业务语义,两者生命周期根本不该绑在一起。
- 不做 TTL,靠业务层天然幂等(唯一约束/状态机):最干净,但前提是每条写操作都能归约到「要么唯一键冲突、要么状态机拒绝」,很多业务做不到(比如「扣减余额 + 发券 + 写流水」三步,中间态没有天然幂等锚点)。
【常见分歧 / 几种做法】
- A 派:Redis/DB 幂等表 + 业务唯一键(msgId 或业务流水号),窗口 7~30 天,窗口外重复视为「新事件」重新处理。
- B 派:只信业务状态机(订单状态只能单向流转,重复消息直接 no-op),幂等表只做审计不做拦截。
- C 派:发送端负责——outbox + 确定性 msgId,消费端只做「最后一条赢」的 upsert,不做主动去重。
我自己在三个项目里分别踩过:
- 第一个:幂等表 TTL=24h,跨天对账补偿触发重复扣款,根因是「补偿事件」和「原始事件」用了不同的 msgId,幂等键对不上。
- 第二个:TTL=7d 对齐 Kafka offsets,消费端 Redis 内存翻倍,且有一次 Redis 主从切换丢 30 分钟去重数据,直接漏拦截。
- 第三个:纯状态机 + outbox,最稳,但要求团队对「哪些字段是幂等锚点」有书面共识,新人接手时容易把「乐观锁版本号」当幂等键用,差点出事。
【我的倾向】
- 幂等窗口不该是一个全局常量,而应该是每条消息类型自己的属性:对账类消息窗口 = 最长补偿周期 + 1 天;实时交易类 = 最长重试周期 + 1 天;审计类可以更长。
- 幂等键应该是「业务流水号」而不是 msgId——msgId 是传输层概念,业务补偿时上游可能换一条新消息重发,msgId 会变,业务流水号不会。
- 幂等拦截和幂等审计要分开:拦截走 Redis(可丢,丢了顶多多处理一次),审计走 DB(不能丢,用于事后对账),两者不要共用一张表。
【想听大家在什么场景下怎么选】
- 你们的幂等窗口是怎么定的?有没有一个「业务方说得出道理」的公式,还是基本靠经验拍?
- 遇到过「幂等表本身成为瓶颈/故障源」的情况吗(Redis 抖动导致拦截失效、去重表扫到慢 SQL 反过来拖垮消费端)?
- 纯状态机派:你们怎么保证「状态只能单向流转」这件事在代码层面真的成立?靠 code review 还是有 lint/测试卡口?
- 有没有人做过「幂等窗口」的可观测性——比如监控「窗口外重复消息」的命中率,用它反过来校准 TTL?
0赞
0收藏
0楼层