重试与幂等必须成对设计:重试解决请求发不出去,幂等解决请求发出后结果不确定;只重试不幂等会重复扣款,只幂等不重试易因瞬时故障失败;推荐Redis SetNX(带NX+EX、业务上下文key、TTL≥最长耗时+余量)为第一道防线,DB唯一索引兜底,Redis故障返回503,客户端须携带稳定Idempotency-Key。

Go 调用外部系统接口时,重试和幂等必须成对设计:重试解决“请求发不出去”,幂等解决“请求发出去了但不确定是否成功”。只做重试不保幂等,会重复扣款;只做幂等不配重试,又容易因瞬时故障失败。关键在于服务端识别重复、客户端控制重试边界。
重试机制:只对可恢复错误生效
外部接口调用失败,不是所有错误都该重试。比如 400(参数错误)、404(资源不存在)是业务问题,重试无意义;而 502/503/504、连接超时、gRPC 的 Unavailable 和 DeadlineExceeded 才属于可恢复异常。
- 用指数退避 + 随机抖动:首次延迟 100ms,之后每次翻倍并加 ±20% 随机偏移,避免大量请求同步冲击
- 限制最大重试次数(通常 3 次),防止长尾请求拖垮上游或下游
- gRPC 场景推荐使用
grpc-ecosystem/go-grpc-middleware/retry,通过WithCodes精确指定重试状态码 - HTTP 场景可封装通用函数,如
RetryWithBackoff(func() error { return doHTTP(req) }, 3),内部用time.Sleep控制间隔
幂等性:服务端原子拦截重复请求
客户端重试的前提,是服务端能稳稳识别“这是同一笔操作”。不能靠前端防连点,也不能靠文档约定——必须由后端强制校验。
- 推荐第一道防线:Redis
SetNX原子指令,key 拼为"idempotent:pay:u1001:abc123"(含业务类型、用户 ID、客户端传的 idempotencyKey),TTL 设为接口最长耗时 + 安全余量(如 60 秒) - key 必须正则校验格式(如
^[a-zA-Z0-9_-]{12,64}$),防注入和非法长度;header 为空直接拒,不 fallback 生成 - Redis 故障时不能放行,必须返回 503 Service Unavailable —— 幂等语义一旦让步,就等于放弃数据一致性底线
- value 存 traceID 或原始 idempotencyKey,方便日志链路追踪;
SetNX返回false表示已存在,不是错误,需主动拦截业务逻辑
兜底策略:数据库唯一约束不可省略
Redis 可能宕机、网络分区、客户端绕过 header,所以 DB 层必须有最后一道防线。
- 在订单、支付、发券等核心表上建联合唯一索引,如
UNIQUE (user_id, idempotency_key) - 插入时捕获唯一冲突错误:
pgx.ErrCodeUniqueViolation(PostgreSQL)或mysql.MySQLError.Number == 1062(MySQL) - 捕获到冲突后,不应返回 409 或静默失败,而应查出原记录,返回 200 + 原始成功结果 —— 让重试方感知“操作已完成”
- 避免用
SELECT FOR UPDATE替代唯一索引,它仅适用于“先查再改”的窄场景,且易锁表、难扩展
客户端配合要点
服务端幂等设计再强,也依赖客户端正确配合。否则重试机制反而成为风险放大器。
- 每次请求必须携带稳定、全局唯一的
Idempotency-Key(如 UUID),且整个重试周期内保持不变 - 不要把 key 放 query string(可能被 CDN 缓存重放),统一走 header,如
X-Idempotency-Key - 若调用的是不支持幂等的第三方 API(如没文档提 Idempotency-Key),就得换策略:先
GET /orders/{id}查状态,确认未处理再发 POST - 本地可记录「请求发起时间 + 请求摘要」到 DB,作为人工对账和故障回溯依据
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











