REST 架构风格
REST是一种基于HTTP协议的Web接口设计风格,其核心思想是通过URL标识资源,使用HTTP方法(GET/POST/PUT/DELETE)表示操作,实现客户端与服务器的无状态交互。REST解决了分布式系统间标准化通信的问题,以统一性和简单性换取通用扩展能力,特别适合CRUD操作但复杂业务建模较困难。关键特性包括无状态、统一接口、资源导向和可缓存性。相比SOAP/GraphQL/gRPC,RES
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
更多推荐
所有评论(0)