接口幂等性需开发者在业务层显式设计,不能依赖HTTP方法语义或框架自动保证;核心是客户端生成唯一idempotency-key,服务端用Redis缓存结果并结合DB唯一约束兜底,且须贯穿分布式调用全链路。
接口幂等性不是靠框架自动保证的,Golang 微服务里必须由开发者在业务逻辑层显式设计和控制。
HTTP 方法 不能直接当作幂等依据很多人误以为 GET 和 PUT 天然幂等、POST 天然不幂等——这在协议语义上成立,但落地到微服务时完全不可信。比如一个 POST /v1/orders 创建订单的接口,若没做任何防重逻辑,前端重复提交、网关重试、客户端崩溃后重发,都会产生多笔相同订单。
DELETE 接口也可能因状态未同步(如数据库软删 + 缓存未失效)导致第二次调用又“删”出副作用PUT 若实现为“全量覆盖”,但上游传入的 updated_at 时间戳每次不同,就可能触发下游审计日志重复写入POST 请求,服务端无感知Go 实现要点核心思路是:把一次请求的唯一性锚定在业务可识别、服务端可校验的标识上,且该标识需具备「全局唯一 + 一次有效」特性。
idempotency-key(如 UUID v4),通过 HTTP Header 传递,服务端用它作为 Redis 的 key 做「操作结果缓存」:首次执行写 DB + 写缓存;后续命中缓存则直接返回上次结果(含状态码、body)
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Idempotency-Key")
if key == "" {
http.Error(w, "missing Idempotency-Key", http.StatusBadRequest)
return
}
// 1. 先查 Redis 是否已有结果
cached, err := h.redis.Get(r.Context(), "idemp:"+key).Result()
if err == nil {
// 命中缓存,原样返回历史响应
var resp struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
json.Unmarshal([]byte(cached), &resp)
w.WriteHeader(resp.Code)
json.NewEncoder(w).Encode(resp)
return
}
// 2. 未命中,执行业务逻辑(注意:DB 层必须有唯一约束,如 order_id 或 external_ref + user_id 联合唯一)
orderID := uuid.New().String()
if err := h.db.CreateOrder(r.Context(), orderID, ...); err != nil {
if errors.Is(err, sql.ErrNoRows) || strings.Contains(err.Error(), "Duplicate entry") {
// 冲突说明已存在,查 DB 获取原始结果并缓存
order, _ := h.db.GetOrderByID(r.Context(), orderID)
result := map[string]any{"code": 200, "msg": "success", "data": order}
data, _ := json.Marshal(result)
h.redis.Set(r.Context(), "idemp:"+key, data, 24*time.Hour)
w.WriteHeader(200)
json.NewEncoder(w).Encode(result)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 3. 成功后缓存结果
result := map[string]any{"code": 201, "msg": "created", "data": map[string]string{"id": orderID}}
data, _ := json.Marshal(result)
h.redis.Set(r.Context(), "idemp:"+key, data, 24*time.Hour)
w.WriteHeader(201)
json.NewEncoder(w).Encode(result)
}
database unique constraint 是最后防线,不是唯一手段仅靠 MySQL 的 UNIQUE KEY (user_id, external_order_no) 或 PostgreSQL 的 ON CONFLICT DO NOTHING 能防止数据重复,但无法解决接口响应不一致问题:第一次成功返回 201,第二次冲突返回 500 或 200(取决于你怎么处理异常),前端无法区分这是“已存在”还是“系统错误”。
SELECT ... FOR UPDATE 防止并发竞态,否则高并发下仍可能双写(即使有唯一索引,报错前的中间态已破坏一致性)GET /v1/user?phone=138...),幂等性天然满足,但要注意缓存雪崩/击穿问题——这不是幂等问题,是可用性问题,别混淆单体应用里加个 Redis + DB 唯一索引还能应付,但在微服务拆分后,一个创建订单操作可能涉及「账户服务扣款」「库存服务锁仓」「通知服务发消息」三个远程调用。此时幂等必须贯穿整条链路:
idempotency-key,不能各自生成idemp::deduct ),否则重试时可能重复扣款idempotency-key 需统一 middleware 注入,避免每个 handler 手动取;HTTP/JSON API 同理,用 Gin/Zap 中间件提前校验并注入上下文真正难的不是某一行代码怎么写,而是所有参与方对「同一请求 = 同一 key + 同一结果」达成共识,并在每个环节都拒绝非幂等行为——哪怕只是日志记录,也得判断 key 是否已存在。