Skip to content

API 设计与网关 ​

接口设计得好不好,半年后维护成本差十倍。这篇讲对外接口的契约和入口层(网关)的职责。

一、REST 设计规范 ​

资源用名词,动作用 HTTP 方法 ​

✅ GET    /users/123
✅ POST   /users
✅ PATCH  /users/123
✅ DELETE /users/123

❌ GET /getUser?id=123
❌ POST /user/delete/123
✅ GET    /users/123
✅ POST   /users
✅ PATCH  /users/123
✅ DELETE /users/123

❌ GET /getUser?id=123
❌ POST /user/delete/123

状态码要真的用起来 ​

码什么时候用
200成功(含 PATCH 更新成功)
201创建成功(配 Location 头)
204成功但无返回体(DELETE)
400参数错误(客户端的问题)
401未认证(没登录/token 无效)
403已认证但无权(越权)
404资源不存在
409冲突(幂等键重复、乐观锁版本冲突)
429触发限流(配 Retry-After)
500服务端错误

别全用 200 + {code: xxx} —— 那样监控、网关、SDK 全都得解析 body 才知道成败。

统一错误体 ​

json
{
  "code": "ORDER_NOT_FOUND",
  "message": "订单不存在,请确认订单号",
  "traceId": "ab12cd34",
  "details": [{ "field": "orderNo", "reason": "not_found" }]
}
{
  "code": "ORDER_NOT_FOUND",
  "message": "订单不存在,请确认订单号",
  "traceId": "ab12cd34",
  "details": [{ "field": "orderNo", "reason": "not_found" }]
}

三个要点:机器可读的 code、人能看懂且可直接展示的 message、traceId 便于排查。

分页与过滤 ​

GET /orders?page=2&size=20&status=PAID&sort=-createdAt
→ { "items": [...], "total": 137, "page": 2, "size": 20 }
GET /orders?page=2&size=20&status=PAID&sort=-createdAt
→ { "items": [...], "total": 137, "page": 2, "size": 20 }

大数据量用游标分页(?cursor=xxx)而不是深分页(原理同 Elasticsearch 的深分页)。

二、版本化 ​

方式例子评价
URL 路径/v1/orders最直观,推荐
请求头Accept: application/vnd.api.v2+json干净,但不好调试
查询参数?version=2不推荐,容易被缓存污染

兼容性原则:加字段是兼容的,删字段/改类型/改语义是破坏性的 —— 破坏性变更才升版本。

三、幂等设计(分布式必问) ​

为什么要幂等:客户端重试、网关超时重发、用户连点,都会造成重复请求。

方法天然幂等吗
GET / PUT / DELETE是(按语义)
POST否,必须自己实现

幂等键方案 ​

1. 客户端生成 requestId(UUID),放在 header:X-Request-Id
2. 服务端:SETNX idempotent:{requestId} → 成功则处理,失败则返回上次结果
3. 处理完把结果缓存(TTL 24h),重复请求直接返回同一结果
1. 客户端生成 requestId(UUID),放在 header:X-Request-Id
2. 服务端:SETNX idempotent:{requestId} → 成功则处理,失败则返回上次结果
3. 处理完把结果缓存(TTL 24h),重复请求直接返回同一结果
java
public Order create(String requestId, CreateReq req) {
    String key = "idem:" + requestId;
    if (!redis.setIfAbsent(key, "1", Duration.ofHours(24))) {
        return loadPreviousResult(requestId);   // 重复请求:返回原结果,不是报错
    }
    try {
        return doCreate(req);
    } catch (Exception e) {
        redis.delete(key);                      // 失败要释放,允许重试
        throw e;
    }
}
public Order create(String requestId, CreateReq req) {
    String key = "idem:" + requestId;
    if (!redis.setIfAbsent(key, "1", Duration.ofHours(24))) {
        return loadPreviousResult(requestId);   // 重复请求:返回原结果,不是报错
    }
    try {
        return doCreate(req);
    } catch (Exception e) {
        redis.delete(key);                      // 失败要释放,允许重试
        throw e;
    }
}

三个易错点:

  1. 重复请求要返回成功的结果,而不是报「重复提交」;
  2. 失败必须释放幂等键,否则用户永远重试不了;
  3. 数据库唯一索引是最后一道防线(Redis 可能丢)。

四、鉴权方式对比 ​

方式适用说明
Session + Cookie传统 Web服务端存储,横向扩展要共享 session
JWT前后端分离、多端无状态,但无法主动失效(要靠黑名单/短 TTL)
OAuth2第三方授权授权码模式最常用
AK/SK 签名开放 API / 服务间见下节

JWT 的三个坑:不能放敏感信息(只 base64,不是加密)、无法主动登出(用短 access + refresh token)、payload 太大会拖慢每个请求。

五、开放 API 的签名与防重放 ​

待签名串 = method + "\n" + path + "\n" + sortedParams + "\n" + timestamp + "\n" + nonce
signature = HMAC-SHA256(secret, 待签名串)
待签名串 = method + "\n" + path + "\n" + sortedParams + "\n" + timestamp + "\n" + nonce
signature = HMAC-SHA256(secret, 待签名串)

服务端校验四件事:

  1. timestamp 与当前时间差 < 5 分钟(防重放);
  2. nonce 未用过(Redis SETNX,TTL 10 分钟);
  3. 签名 一致;
  4. AK 有权限访问该接口。

六、网关的五项职责 ​

客户端 → 网关(路由 / 鉴权 / 限流 / 灰度 / 日志)→ 下游服务
客户端 → 网关(路由 / 鉴权 / 限流 / 灰度 / 日志)→ 下游服务
职责具体做什么不做会怎样
路由按 path/header 转发,做协议转换客户端要记住 N 个服务地址
鉴权统一验 token、验签,透传用户身份每个服务都要写一遍鉴权
限流熔断按用户/租户/接口限流,失败熔断一个服务挂掉拖垮全站
灰度按 header/比例把流量打到新版本只能靠发版扩容做灰度
日志与追踪统一记录 traceId、耗时、状态码出问题不知道该找谁

网关不该做的事:写业务逻辑、做数据聚合(那是 BFF 的职责)、做重计算。

实现参考:Spring Cloud Gateway 的路由与过滤器。

七、灰度发布的四种做法 ​

方式粒度实现
按比例5% → 20% → 100%网关权重路由
按用户白名单用户先体验网关读用户标签
按请求头内部测试带 X-Gray: 1网关匹配 header
按机房/地域单机房先上DNS / 网关按 region

灰度必须配套两件事:可比对的指标(新老版本的错误率、P99)和一键回滚。

八、接口设计的十条检查 ​

  1. 资源用名词、层级不超过 3 层
  2. 状态码真实反映结果,别全 200
  3. 错误体有 code / message / traceId
  4. 列表接口必须有分页和上限(默认 20,最大 100)
  5. POST 有幂等键
  6. 时间统一 ISO8601 + 带时区,金额统一用「分」的整数
  7. 枚举值有文档,且不随意改语义
  8. 破坏性变更升版本号
  9. 敏感字段不入参/不出参(脱敏)
  10. 每个接口有超时和重试策略

九、速查 ​

问题答案
REST 路径怎么写名词 + HTTP 方法,别在 URL 里放动词
状态码要不要用要用,401/403/409/429 都别退化成 200
幂等怎么实现客户端 requestId + Redis SETNX + 数据库唯一索引兜底
重复请求返回什么返回上次成功的结果,不是报错
JWT 最大的问题无法主动失效,只能短 TTL + 刷新令牌
网关做什么路由、鉴权、限流熔断、灰度、日志追踪
网关不做什么业务逻辑、数据聚合、重计算
开放 API 怎么防重放timestamp + nonce + 签名