【背景】
上周处理一个支付回调重复消费的问题。下游对账系统用 Redis 做幂等去重,key 格式 dedup:{orderId}:{eventType},TTL 设了 5 分钟。结果一笔订单在 8 分钟后因为上游网络抖动重试,Redis key 已过期,去重失效,重复入账。
事故不大(手动冲正了),但它暴露了一个我们团队从来没认真对齐过的设计决策:幂等去重的 TTL 到底该怎么定? 这不是一个「查文档就能找到答案」的问题,因为它同时取决于上游重试策略、消息积压窗口、GC 停顿、跨机房延迟……而这几样东西,很多时候不在你团队的掌控范围内。
【我观察到的三种主流做法】
做法 A:短 TTL(5 ~ 30 分钟) 逻辑:「上游重试窗口就那么长,过了这个窗口再发就是 bug,应该报警而不是静默吞掉。」 典型场景:Kafka at-least-once 下的重复投递,正常重试窗口 < 5 分钟;HTTP 回调重试间隔 30s/60s/120s,总共 3 次。 风险:一旦上游重试策略改了、消息积压了、或者跨 region 延迟飙了,5 分钟就不够用了。
做法 B:长 TTL(1 天 ~ 7 天)
逻辑:「宁可多存 7 天的去重 key,也不能赌上游行为。」
实现:Redis SETEX 7d,或者落一张 idempotency_keys 表 + 定时清理。
代价:key 数量 = 峰值 QPS × TTL,200k msg/s × 7 天 ≈ 1.2 亿 key,Redis 内存直接爆炸;落 DB 的话,这张表的写入本身就是个热点。
做法 C:业务状态机幂等(无 TTL)
逻辑:不依赖时间窗口,而是看业务状态——订单状态从 PAID → SHIPPED 之后,再来一次 PAID 事件直接丢弃,跟时间无关。
优点:天然幂等,没有 TTL 问题;去重逻辑和业务语义绑定,不会「过期后重复」。
代价:每个消费者入口都要自己实现状态机判断,没有统一基础设施;如果状态机设计有 bug(比如漏了某个中间态),幂等就漏了。
【我的倾向】
我偏向做法 C(业务状态机),但有一个前提:团队有纪律,每个消费者入口都走统一的幂等中间件(比如一个 AOP / 拦截器,自动从消息体提取幂等 key + 状态机断言),而不是各写各的。
如果团队规模小、没有中间件基建、或者业务状态机太复杂(20+ 状态),我会退而求其次选做法 B,但 TTL 至少 24 小时,并且加一条告警:同一个幂等 key 在 TTL 后半段被命中(说明上游重试窗口可能超过了 TTL 的一半),这是 TTL 设短了的领先信号。
做法 A 我只在「上游重试策略是确定的、且在我掌控范围内」的时候才用。
【一个我没想清楚的问题】
在 200k msg/s 的吞吐下,做法 C 的幂等判断(一次 DB SELECT + 条件 UPDATE)和做法 B 的幂等判断(Redis SETNX),性能差距到底有多大?我直觉是 DB 路径在 P99 上会差 2~5 倍,但没找到靠谱的 benchmark。有没有人实际测过?如果差距没想象中大,做法 C 的「无 TTL」优势是不是就完全压过了?
【想听大家聊】
- 你们线上幂等去重的 TTL 是怎么定的?有没有踩过「TTL 太短 → 重复消费」的坑?具体是什么场景触发的?
- 跨机房 / 多 region 部署下,去重 key 放本地 Redis 还是共享存储(Redis Cluster / 分布式 DB)?本地放的话,两个 region 的 key 不一致怎么收敛?
- 「业务状态机幂等」和「去重表 / 去重 key 幂等」,你们团队最终怎么选?依据是什么——是性能、是团队规模、还是业务复杂度?
- 有没有人做过「幂等判断本身的开销」在总处理时间里的占比 benchmark?如果幂等判断占了单条消息处理时间的 10%+,你们怎么权衡?