幂等设计到底该放在哪一层:网关、服务、还是存储?
帖子正文与楼层回复。
背景
超时重试是分布式系统的标配,而幂等就是重试的解药。但"在哪一层做幂等"这件事,我在 5 年里至少换过三个团队,三个团队的姿势完全不同,每次迁移都要重写一遍。
常见分歧大致有四种做法:
做法 A:网关层统一兜底。 在 API Gateway / BFF 层解析 Idempotency-Key header,命中 Redis 缓存就直接返回上次的响应。优点是一次实现、全服务受益;缺点是网关变成"业务无状态"的假设被打破,网关挂了或 Redis 抖动时幂等就失效了,而且不同业务的幂等语义差异很大(支付要精确一次,日志上报只要最终一致),网关很难一刀切。
做法 B:服务层各自实现。 每个微服务在自己的 Controller / Service 里做幂等判断,通常是一张 idempotency_record 表或 Redis SETNX。优点是语义最精确、每个服务自己定义"什么算重复";缺点是 N 个服务 N 套实现,质量参差不齐,而且一旦某个服务忘了做,线上就出事故。
做法 C:存储层兜底。 靠数据库唯一索引(UNIQUE KEY)或乐观锁(version 字段)做最后防线。优点是零额外组件、最可靠;缺点是错误信息不友好("Duplicate entry" 对调用方不透明),而且只防"写重复",防不了"副作用重复"(比如已经发了 MQ 消息、已经扣了库存)。
做法 D:分层组合。 网关做粗粒度拦截(防网络层重放),服务做业务级幂等(防逻辑重复),存储做最终兜底(防并发竞态)。理论最完整,但工程成本也最高。
我的倾向
我倾向 D,但有一个硬规则:每一层的幂等"所有权"必须写进接口契约文档里。具体来说:
- 网关层只负责"网络重放"(同一个
Idempotency-Key在 5 分钟内重复到达),不关心业务语义; - 服务层负责"业务幂等",幂等键的生成规则(
orderId + action还是userId + timestamp)必须写在 OpenAPI / 接口文档里,调用方自己生成,服务端校验; - 存储层只做"并发兜底",唯一索引是最后防线,不是主防线;
- 每一层都要有监控:网关拦截率、服务幂等命中率、存储层 duplicate key 异常数。这三个数任何一个突然飙升都是信号。
核心理由:幂等不是"加个缓存",而是容量规划的一部分——跟超时、重试一样,它影响的是系统的一致性边界。把所有权模糊掉,出了事故就是"谁该防"的扯皮会。
想听大家聊聊
- 你们团队实际是哪种姿势?有没有从 A 迁到 B(或反过来)的经历,迁移时踩过什么坑?
- 在"最终一致"场景(比如 MQ 消费、事件溯源)里,幂等键怎么设计才不会把"合法重试"误杀成"重复"?
- 有没有人试过在 Sidecar / Service Mesh 层做幂等(比如 Envoy filter)?效果怎么样?
- 你们怎么向调用方解释"幂等键应该由谁生成"?是网关自动生成还是调用方自己传?
欢迎把你们的避坑清单甩过来,我攒一个"幂等设计 Checklist"出来。