REST 架构风格


0. 定位声明

适用版本:REST 不绑定软件版本;参考 HTTP/1.1(RFC 7230-7235)、HTTP/2(RFC 7540)
          及 Roy Fielding 2000 年博士论文
前置知识:HTTP 协议基础(请求/响应、状态码、Header)、基本 Web 开发经验
不适用范围:不覆盖 GraphQL、gRPC、SOAP;不适用于实时双向通信(WebSocket);
           不是具体框架(Spring MVC、FastAPI)的使用手册

1. 一句话本质

REST 是一套"用地址表示资源,用 HTTP 动词表示操作,用响应体传递状态"的 Web 接口设计规则。

你告诉服务器"我想对哪个东西(URL)做什么(GET/POST/PUT/DELETE)",服务器把结果(通常 JSON)还给你,双方不需要记住对话的上下文。

它解决的问题:如何让分布在全球的不同系统,用统一的方式互相"说话",而不被某种语言、操作系统或平台绑死。


2. 背景与根本矛盾

2.1 历史背景

  • 1991 年:WWW 诞生,HTTP+HTML 将互联网变成信息高速公路
  • 1990 年代末:系统间通信主流用 SOAP/XML-RPC——把远程调用包装成 XML,极其臃肿(一个简单查询 XML 可达数 KB)
  • 2000 年:Roy Fielding 博士论文提出 REST,分析 WWW 成功原因后提炼为约束集合,证明"HTTP 本身已足够强大,无需再包一层 RPC"
  • 2006-2010 年:Twitter、Facebook、Stripe 等开放公共 REST API,REST 成为 Web API 事实标准

2.2 根本矛盾(Trade-off)

约束 获得的收益 付出的代价
无状态 无会话,水平扩展无限制,故障恢复简单 每次请求携带完整上下文,网络流量增大
统一接口 客户端与服务端解耦,可独立演进 牺牲效率;复杂操作难以"套进"HTTP 动词
资源导向 URL 语义清晰,可缓存,易文档化 设计复杂业务动作(如"审批")有摩擦

核心 Trade-off:REST 用统一性和简单性换取通用扩展能力,代价是表达能力受限——擅长 CRUD,复杂业务流程建模有摩擦。


3. 核心概念与领域模型

3.1 关键术语表

术语 费曼式定义 正式定义
资源 你想操作的"东西",如一篇文章、一个用户 任何有意义的命名信息,通过 URI 唯一标识
表述 服务器用某种格式(JSON/XML)把"东西"快照给你 资源在某时刻的状态序列化形式
状态转移 服务器把资源新状态传回,客户端据此更新视图 通过传输资源表述驱动客户端应用状态变化
统一接口 不管操作什么,方法都一样(GET/POST/PUT/DELETE) 通过标准化接口解耦客户端与服务端
无状态 服务器不记得你上次说了什么,每次请求要"自我介绍" 会话状态完全由客户端维护
HATEOAS 服务器返回数据时告诉你"下一步可以做什么" 通过超媒体链接驱动应用状态机

3.2 六大架构约束

REST 六大约束
├── 1. 客户端-服务器:UI 与数据存储分离,独立演进
├── 2. 无状态:每次请求自包含,会话在客户端
├── 3. 可缓存:响应需声明缓存策略(Cache-Control, ETag)
├── 4. 分层系统:客户端不感知中间层(CDN、网关、负载均衡)
├── 5. 统一接口
│   ├── 资源标识(URI)
│   ├── 通过表述操作资源
│   ├── 自描述消息
│   └── HATEOAS
└── 6. 按需代码(可选):服务端可下发可执行代码

3.3 Richardson 成熟度模型

Level 0:HTTP 作为隧道(只有一个 URL,全用 POST)
Level 1:引入资源(多个 URL,但只用 POST)
Level 2:正确使用 HTTP 动词和状态码  ← 业界大多数"REST API"在此
Level 3:HATEOAS(响应含可用操作链接)← 真正的 REST,极少落地

3.4 HTTP 动词语义

动词 语义 幂等性 安全性
GET 读取
POST 创建/触发动作
PUT 全量替换
PATCH 部分更新 ❌(实现相关)
DELETE 删除

幂等性是生产系统关键属性:幂等操作可在网络超时后安全重试,不会产生副作用。


4. 对比与选型决策

4.1 横向对比

维度 REST GraphQL gRPC SOAP
协议 HTTP/1.1, HTTP/2 HTTP HTTP/2(必须) HTTP等
数据格式 JSON/任意 JSON Protobuf(二进制) XML
类型系统 无(靠OpenAPI) 强类型Schema 强类型(.proto) WSDL
学习曲线 中-高
Over-fetching 常见 常见
浏览器原生支持
序列化性能 高(Protobuf比JSON快5-10倍)
典型场景 公开API、Web/移动端 复杂前端、BFF 微服务内部通信 企业遗留集成

4.2 选型决策树

需要对外开放公共 API?
├── 是 → REST(生态最广,开发者接受度最高)
└── 否(内部微服务通信)
    ├── 需要高性能低延迟(<10ms)或强类型契约? → gRPC
    ├── 前端数据需求复杂多变,字段频繁裁剪? → GraphQL
    └── 以上都不是 → REST(简单够用)

需要实时双向通信(聊天、游戏)?→ WebSocket/SSE,REST 不适用

5. 工作原理与实现机制

5.1 URL 资源树设计

/users                           # 用户集合
/users/{userId}                  # 单个用户
/users/{userId}/orders           # 该用户的订单集合
/users/{userId}/orders/{orderId} # 该用户的某个订单

设计原则:URI 用名词不用动词;层级不超过 3 层;复数名词表示集合(/articles

5.2 典型请求-响应时序

GET 读取资源:

客户端                        服务器
  │── GET /users/42 ────────►│
  │   Authorization: Bearer  │── 1. 验证 Token
  │                          │── 2. 查询数据库
  │◄─ 200 OK ────────────────│
  │   ETag: "abc123"         │
  │   Cache-Control: max-age=300
  │   { "id": 42, ... }      │

POST 创建资源:

客户端                        服务器
  │── POST /users ──────────►│── 1. 验证请求体
  │   { "name": "Alice" }   │── 2. 写入数据库
  │◄─ 201 Created ───────────│
  │   Location: /users/43    │  ← 告知新资源地址

ETag 乐观锁更新(防并发覆盖):

客户端                        服务器
  │── GET /articles/1 ──────►│
  │◄─ 200 OK, ETag: "v3" ───│
  │── PUT /articles/1 ──────►│
  │   If-Match: "v3"         │── ETag 匹配?
  │   { "title": "新标题" } │   ├── 是 → 200 更新成功
  │                          │   └── 否 → 409 Conflict

5.3 HTTP 状态码速查

状态码 语义 场景
200 成功 GET/PUT/PATCH
201 创建成功 POST 创建资源
204 成功无响应体 DELETE
400 请求参数错误 客户端传参有误
401 未认证 缺少/无效 Token
403 无权限 已认证但无权访问
404 资源不存在 资源ID不存在
409 资源冲突 ETag不匹配
422 语义错误 格式对但业务规则不通过
429 限流 超速率限制
500 服务端错误 未预期异常

5.4 三个关键设计决策

决策一:无状态 vs 有状态
有状态方案(Session)扩容需要粘性会话或 Redis 会话共享,故障恢复复杂。REST 每次携带完整 JWT Token,任意实例可处理,天然水平扩展。代价:Token 验证约 0.1-1ms 开销,且 Token 无法即时吊销。

决策二:JSON vs 二进制格式
JSON 可读性强,开发者体验好;Protobuf 体积小 3-10 倍但不可读。公开 API 首要目标是互操作性而非极致性能。权衡点:单次请求体 >100KB 或 QPS >50,000 时考虑 gRPC。

决策三:为何 HATEOAS 很少落地
客户端开发者不愿实现"发现链接"逻辑,宁愿查文档;链接解析增加复杂度和网络往返。OpenAPI/Swagger 文档成为了 HATEOAS 的"人工替代品"。


6. 高可靠性保障

6.1 幂等性设计(核心手段)

POST 默认非幂等,重试会导致重复下单等问题。幂等 Key 方案

POST /orders
Idempotency-Key: client-generated-uuid-abc123

{ "product_id": 1, "qty": 2 }

服务端将 Key 存入 Redis(TTL 24h),相同 Key 重复请求直接返回第一次结果。Stripe、Adyen 等支付公司均采用此模式。

6.2 限流与熔断

  • 限流:令牌桶算法,典型:1000 req/min per API Key,超限返回 429 + Retry-After: 60
  • 熔断:连续失败率 >50%(5s 窗口)开路,30s 后半开探测

6.3 可观测性核心指标

指标 正常阈值 告警阈值
P99 响应时间 <200ms >1000ms
P50 响应时间 <50ms >200ms
5xx 错误率 <0.1% >1%
可用性 >99.9% <99.5%

7. 使用实践与故障手册

7.1 URL 设计规范

✅ 正确
GET    /v1/articles
POST   /v1/articles
GET    /v1/articles/123
PUT    /v1/articles/123
DELETE /v1/articles/123
POST   /v1/articles/123/publish   # 业务动作用"子资源动词"建模

❌ 错误
GET  /getArticle?id=123           # 动词在 URL 中
POST /article/doPublish           # 非 RESTful 动词路径

响应体规范(RFC 7807 错误格式):

{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The 'email' field must be a valid email address.",
  "request_id": "req-abc-xyz"
}

7.2 故障模式手册

【故障一:N+1 查询问题】
- 现象:GET /orders 返回100条,响应时间>2s,DB QPS飙升100倍
- 根本原因:每条订单触发一次关联查询,100条=101次DB查询
- 预防:API 支持 ?include=user,服务端批量查询(IN子句)
- 应急:临时降低 per_page 上限;添加覆盖索引

【故障二:PUT 并发覆盖(丢失更新)】
- 现象:并发用户更新同一资源,前者修改无声无息丢失
- 根本原因:PUT 未实现乐观锁,无条件覆盖
- 预防:所有可写资源返回 ETag,PUT 请求要求 If-Match Header
- 应急:数据库 version 字段乐观锁;资源版本号机制

【故障三:Token 泄露】
- 现象:Bearer Token 出现在日志/URL 中被截获
- 根本原因:Token 放 URL 而非 Header;JWT 无过期时间
- 预防:Token 只放 Authorization Header;JWT exp 设 15-60min;HTTPS 强制
- 应急:Redis 黑名单使 Token 失效;强制重新登录

【故障四:Breaking Change 破坏兼容性】
- 现象:API 升级后老版本移动 App 大量报错
- 根本原因:修改已发布字段类型/语义;未做版本隔离
- 预防:URI 版本化(/v1, /v2);只加不删;Deprecated Header + 6个月迁移窗口
- 应急:保留旧版路由;API Gateway 按版本路由

【故障五:无限制分页导致 OOM】
- 现象:GET /logs?per_page=100000 服务端内存溢出
- 根本原因:分页上限未约束
- 预防:强制 per_page 上限(max=1000);超10万行改游标分页
- 应急:API Gateway 层注入上限校验

7.3 边界条件与局限性

  • 实时通信:REST 不适合,用 WebSocket/SSE
  • 复杂查询:多维过滤用 REST 参数表达极丑陋,考虑 GraphQL
  • 大文件上传:>100MB 需分片上传(Multipart),设计复杂度剧增
  • 深分页失效OFFSET 900000 LIMIT 20 全表扫描,P99 延迟可达 5s 以上,必须改用游标分页(Keyset Pagination)

8. 性能调优指南

8.1 瓶颈分层排查

响应慢 → 分层排查
├── 网络层:DNS解析(>100ms?) / TLS握手 / 传输延迟
├── API Gateway:限流配置 / 路由规则
├── 应用层:序列化时间 / 业务逻辑
├── 数据库层:慢查询(>10ms) / N+1 / 缺索引
└── 外部依赖:第三方 API 超时

8.2 调优步骤(按优先级)

优先级 手段 量化目标 验证方法
P0 开启 HTTP/2 RPS 提升 30-50% curl --http2 -v
P0 开启 Gzip/Brotli JSON 体积减少 60-80% 检查 Content-Encoding
P1 Cache-Control 优化 CDN 命中率 >90% CDN 监控面板
P1 DB 连接池调优 连接等待 <5ms 监控 pool wait time
P2 游标分页替代 OFFSET 深翻页 P99: 5s→50ms EXPLAIN 执行计划
P2 异步化耗时操作 接口响应: 3s→200ms 改 202+轮询/webhook

8.3 Nginx 配置参考(1.18+)

http {
    keepalive_timeout 65;
    keepalive_requests 1000;
    
    gzip on;
    gzip_min_length 1k;
    gzip_comp_level 6;        # 1-9,6为性能/压缩率平衡点
    gzip_types application/json text/plain;
    
    upstream api_backend {
        keepalive 32;
        server 127.0.0.1:8080;
    }
}

9. 演进方向与未来趋势

HTTP/3(QUIC):基于 UDP 消除 TCP 队头阻塞,在高丢包环境(移动网络)P99 延迟可降低 30-40%。REST API 无需修改业务逻辑即可受益。Cloudflare、Nginx 1.25+、Envoy 已支持。

OpenAPI 3.1 + Design-First:与 JSON Schema 完全对齐,SDK 自动生成、Mock Server、契约测试(Pact/Schemathesis)生态成熟。从"写代码实现"转向"先设计 Schema 再生成代码",可将 API 集成联调时间缩短 50% 以上。

REST + AI Agent:REST API 成为 MCP(Model Context Protocol)和 Function Calling 的主要载体,API 的"机器可读性"重要性上升,HATEOAS 思想在 AI 时代可能迎来新的实践机会。


10. 面试高频题

【基础理解层】

Q:REST 和 RESTful API 有什么区别?
A:REST 是 Roy Fielding 定义的架构约束风格;RESTful API 是"声称遵循 REST 风格"的 Web API。严格来说大多数"RESTful API"只实现了 Richardson 成熟度 Level 2(正确使用 HTTP 动词和状态码),未实现 HATEOAS,并非真正的 REST。
考察意图:对 Fielding 原始定义的理解深度;区分概念的精确性

Q:401 和 403 的区别?
A:401 = “你是谁我不认识”(未认证,需提供凭证);403 = “我认识你但你没权限”(已认证,但无权访问)。
考察意图:认证(Authentication)vs 授权(Authorization)概念区分


【原理深挖层】

Q:REST 无状态设计带来了哪些问题?如何解决?
A:① Token 无法即时失效(JWT 自包含,签发后无法撤回)→ 短期 Access Token(15min)+ Refresh Token + Redis 黑名单;② 每次携带完整 Token,流量增大 → Token 压缩,网关层解密后内部不传递;③ 用户行为连续性依赖客户端维护。
考察意图:是否真正理解无状态的代价及工程应对方案

Q:为什么 PATCH 不一定是幂等的?
A:PATCH 语义由实现决定。{ "title": "新标题" } 是幂等的;{ "score": "+10" }(相对增量)是非幂等的。RFC 5789 明确说明 PATCH “不一定是幂等的”。
考察意图:是否对 RFC 有深入理解;区分抽象定义和具体实现


【生产实战层】

Q:如何设计 API 版本管理策略?
A:三种方案:① URI 版本化(/v1/users,最直观,缓存友好);② Header 版本化(Accept: application/vnd.api.v2+json,语义严格但不直观);③ 查询参数(?version=2,容易被忽略)。生产推荐 URI 版本化,遵循向后兼容原则(只加不删),旧版本废弃前维护至少 12 个月,通过 Sunset Header 给迁移通知。
考察意图:API 治理能力;是否有版本迁移实际经验

Q:OFFSET 分页和游标分页各适用什么场景?
A:OFFSET 分页支持跳页,实现简单,但数据量大时性能差(OFFSET 100000 全表扫描);游标分页无论第几页性能恒定(O(log n) 索引查找),不支持跳页。数据量 <10 万用 OFFSET,超过 10 万行、实时性要求高(社交 Feed、日志)用游标分页。
考察意图:数据库性能意识;是否遇到过深翻页问题


11. 文档元信息

验证声明

✅ 与官方文档一致性核查:
   - Roy Fielding 博士论文:https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
   - RFC 9110(HTTP语义):https://tools.ietf.org/html/rfc9110
   - RFC 7807(Problem Details):https://tools.ietf.org/html/rfc7807
   - RFC 5789(PATCH):https://tools.ietf.org/html/rfc5789
   - Richardson 成熟度模型:https://martinfowler.com/articles/richardsonMaturityModel.html

⚠️ 未经本地实测,基于文档与业界经验推断:
   - Gzip 压缩 60-80%:实际受 JSON 结构影响,通常 60-75%
   - HTTP/3 降低 30-40% 延迟:来自 Cloudflare 等公开测试报告
   - 深分页 5s 延迟:受 DB 配置和硬件影响,仅为量级参考

参考资料

【官方文档/RFC】
- Roy Fielding 博士论文(REST 原典):https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
- RFC 9110:https://tools.ietf.org/html/rfc9110
- OpenAPI 3.1:https://spec.openapis.org/oas/v3.1.0

【延伸阅读】
- 微软 API 设计指南:https://github.com/microsoft/api-guidelines
- Google API 设计指南:https://cloud.google.com/apis/design
- GitHub REST API(优秀范例):https://docs.github.com/en/rest
- Stripe API(业界标杆):https://stripe.com/docs/api

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐