第5章:使用REST API通信(Communicating with REST)
一、导读
1.1 本章学习目标
REST 是微服务间同步通信最常用的方式。第5章详细讲解 RESTful API 的设计原则和微服务场景下的特殊挑战。Richardson 从 REST 成熟度模型出发,逐步深入到资源建模、幂等性设计、API 版本管理、以及容错机制等实战话题。
本章的核心学习目标包括:
- 掌握 REST 成熟度模型:从 Level 0 到 Level 3 的演进路径。
- 学会资源建模:如何将业务概念映射为 REST 资源。
- 理解幂等性设计:为什么幂等性对微服务至关重要,如何实现。
- 掌握 API 版本管理策略:URI 版本 vs Header 版本的权衡。
- 理解容错模式:重试、超时、熔断器在 REST 调用中的应用。
- 了解 gRPC 作为 REST 的替代方案:适用场景和优劣势。
1.2 为什么本章如此重要
虽然消息传递(第4章)在微服务解耦方面具有优势,但同步的 REST 调用仍然是微服务通信的基础。大多数微服务架构同时使用同步和异步两种通信方式。Richardson 指出:REST 是微服务通信的"默认选择"——它简单、通用、跨语言支持好,是团队最容易上手的通信方式。
本章的重要性在于它不仅讲解 REST 的理论,更深入讨论微服务场景下的实战问题:服务间调用的容错、幂等性保证、API 版本管理等。这些问题在单体架构中不存在,但在微服务中是每个团队都必须面对的。
1.3 核心问题引导
如何将业务概念正确地建模为 REST 资源?
为什么 POST 请求的幂等性设计如此困难?有哪些解决方案?
API 版本管理应该用 URI 路径还是 HTTP Header?各有什么优缺点?
熔断器的参数(失败率阈值、等待时间、滑动窗口)如何确定?
gRPC 在什么场景下比 REST 更有优势?
如何设计一个微友好的 REST API?
二、核心概念详解
2.1 REST 资源建模
2.1.1 资源识别
REST 的核心抽象是"资源"(Resource)。每个资源由一个 URI 标识,通过 HTTP 方法进行操作。
// 资源建模原则:
// 1. 名词用 URI 表示(资源)
// 2. 动词用 HTTP 方法表示(操作)
// 3. 集合用复数名词
// 4. 层级关系用路径表示
// FTGO 订单服务的资源建模:
// 订单集合:GET /api/orders
// 单个订单:GET /api/orders/{orderId}
// 订单的子资源:GET /api/orders/{orderId}/items
// 订单的操作:POST /api/orders/{orderId}/cancel
// 创建订单:POST /api/orders
// 更新订单:PUT /api/orders/{orderId}
// 部分更新:PATCH /api/orders/{orderId}
// 删除订单:DELETE /api/orders/{orderId}
// 反模式:RPC 风格的 URI
// POST /api/getOrder ← 动词不应该出现在 URI 中
// POST /api/createOrder ← 应该用 POST /api/orders
// POST /api/updateOrderStatus ← 应该用 PATCH /api/orders/{id}2.1.2 HTTP 方法语义
| 方法 | 语义 | 幂等性 | 安全性 | 请求体 | 响应 |
|---|---|---|---|---|---|
| GET | 查询资源 | 是 | 是 | 无 | 资源表示 |
| POST | 创建资源/执行操作 | 否 | 否 | 资源表示 | 新资源 |
| PUT | 全量替换资源 | 是 | 否 | 完整资源 | 更新后资源 |
| PATCH | 部分更新资源 | 视情况 | 否 | 变更集 | 更新后资源 |
| DELETE | 删除资源 | 是 | 否 | 无 | 204 No Content |
2.1.3 HTTP 状态码
// 常用状态码:
// 2xx 成功:
// 200 OK → GET/PUT/PATCH 成功
// 201 Created → POST 创建成功
// 204 No Content → DELETE 成功
// 3xx 重定向:
// 301 Moved Permanently → 资源永久移动
// 304 Not Modified → 缓存有效
// 4xx 客户端错误:
// 400 Bad Request → 请求参数错误
// 401 Unauthorized → 未认证
// 403 Forbidden → 无权限
// 404 Not Found → 资源不存在
// 409 Conflict → 资源冲突(如重复创建)
// 422 Unprocessable → 业务校验失败
// 429 Too Many Requests → 限流
// 5xx 服务端错误:
// 500 Internal Error → 服务内部错误
// 502 Bad Gateway → 网关错误
// 503 Service Unavailable → 服务不可用2.2 幂等性设计
2.2.1 为什么需要幂等性
在微服务中,网络不可靠是常态。客户端发送请求后可能因为超时不知道请求是否成功,只能重试。如果被调用的 API 不是幂等的,重试可能导致重复操作。
// 非幂等的 POST 请求:
// POST /api/orders
// { "customerId": "456", "items": [...] }
//
// 客户端发送请求 → 超时(不知道是否成功)
// 客户端重试 → 创建了两个订单!
//
// 解决方案:使用 Idempotency-Key
// POST /api/orders
// Idempotency-Key: req-abc-123
// Content-Type: application/json
// { "customerId": "456", "items": [...] }
//
// 服务端处理逻辑:
// 1. 检查 Redis 中是否存在 key = "req-abc-123" 的记录
// 2. 如果存在 → 返回之前保存的响应
// 3. 如果不存在 → 处理请求
// a. 创建订单
// b. 保存响应到 Redis(key = "req-abc-123", value = 响应)
// c. 返回响应2.2.2 幂等性实现方案
// 方案1:Idempotency-Key + Redis
@RestController
public class OrderController {
@Autowired
private RedisTemplate<String, String> redis;
@PostMapping("/api/orders")
public ResponseEntity<?> createOrder(
@RequestHeader("Idempotency-Key") String idempotencyKey,
@RequestBody CreateOrderRequest request) {
String cached = redis.opsForValue().get(idempotencyKey);
if (cached != null) {
return ResponseEntity.ok(cached);
}
Order order = orderService.create(request);
String response = serialize(order);
redis.opsForValue().set(idempotencyKey, response,
Duration.ofHours(24));
return ResponseEntity.status(201).body(order);
}
}
// 方案2:数据库唯一约束
// 使用业务唯一键(如 orderId = customerId + timestamp + random)
// 数据库 UNIQUE 约束保证不会重复插入2.3 API 版本管理
2.3.1 版本策略对比
// 方案1:URI 路径版本
// GET /v1/api/orders
// GET /v2/api/orders
// 优点:直观、易于路由
// 缺点:URI 不干净、版本多了管理困难
// 方案2:Header 版本
// GET /api/orders
// Accept: application/vnd.ftgo.v1+json
// 优点:URI 干净
// 缺点:不直观、调试困难
// 方案3:查询参数版本
// GET /api/orders?version=1
// 优点:简单
// 缺点:不符合 REST 规范
// Richardson 的建议:
// 内部服务间通信:URI 路径版本(简单直接)
// 对外 API:Header 版本(更 RESTful)
// 关键原则:向后兼容,尽量避免破坏性变更2.3.2 向后兼容原则
// 可以做的变更(向后兼容):
// 1. 添加新的可选字段
// 2. 添加新的 API 端点
// 3. 添加新的可选查询参数
// 4. 放宽已有参数的校验
// 不能做的变更(破坏性):
// 1. 删除或重命名字段
// 2. 修改字段类型
// 3. 修改已有 API 的语义
// 4. 将可选参数改为必填
// 5. 修改 URL 路径2.4 容错机制
2.4.1 重试策略
// Resilience4j 重试配置
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(100))
.intervalFunction(IntervalFunction.ofExponentialBackoff(100, 2.0))
.retryExceptions(IOException.class, TimeoutException.class)
.ignoreExceptions(HttpClientErrorException.class)
.build();
Retry retry = Retry.of("orderService", retryConfig);
// 使用重试调用远程服务
Order result = Retry.decorateSupplier(retry, () -> {
return orderClient.getOrder(orderId);
}).get();2.4.2 熔断器模式
// 完整的熔断器配置
CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slowCallRateThreshold(80)
.slowCallDurationThreshold(Duration.ofSeconds(5))
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(5)
.slidingWindowSize(10)
.minimumNumberOfCalls(5)
.build();
// 结合重试和熔断
CircuitBreaker circuitBreaker = CircuitBreaker.of("paymentService", cbConfig);
Retry retry = Retry.of("paymentService", retryConfig);
// 组合使用:先熔断,再重试
Supplier<Order> decoratedSupplier = CircuitBreaker
.decorateSupplier(circuitBreaker, () -> paymentClient.createPayment(order));
decoratedSupplier = Retry
.decorateSupplier(retry, decoratedSupplier);
Order result = Try.ofSupplier(decoratedSupplier)
.recover(throwable -> fallbackPayment(order))
.get();2.5 gRPC 简介
// gRPC 服务定义
syntax = "proto3";
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (Order);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc WatchOrders(WatchRequest) returns (stream OrderEvent);
rpc ProcessOrders(stream OrderRequest) returns (stream OrderResult);
}
// gRPC vs REST 对比:
// 特性 | gRPC | REST
// 协议 | HTTP/2 | HTTP/1.1
// 数据格式 | Protocol Buffers | JSON
// 性能 | 高(二进制序列化) | 中(文本序列化)
// 类型安全 | 强(.proto 定义) | 弱(运行时校验)
// 流式传输 | 原生支持 | 需要 SSE/WebSocket
// 浏览器支持 | 需要 gRPC-Web | 原生支持
// 调试 | 困难 | 简单
// gRPC 适用场景:
// 1. 内部服务间高性能通信
// 2. 多语言环境(.proto 自动生成客户端代码)
// 3. 流式传输(实时数据推送)
// 4. 低延迟要求三、实战案例
3.1 案例一:完整的 REST API 设计
// 订单服务的完整 REST API
@RestController
@RequestMapping("/api/orders")
public class OrderApiController {
@GetMapping
public Page<OrderView> listOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String status) {
return orderService.listOrders(page, size, status);
}
@GetMapping("/{id}")
public ResponseEntity<OrderDetail> getOrder(@PathVariable String id) {
return orderService.getOrderDetail(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Order> createOrder(
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
@RequestBody @Valid CreateOrderRequest request) {
Order order = orderService.createOrder(request, idempotencyKey);
return ResponseEntity.status(201)
.header("Location", "/api/orders/" + order.getId())
.body(order);
}
@PatchMapping("/{id}")
public ResponseEntity<Order> updateOrder(
@PathVariable String id,
@RequestBody @Valid UpdateOrderRequest request) {
return orderService.updateOrder(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping("/{id}/cancel")
public ResponseEntity<Void> cancelOrder(@PathVariable String id) {
orderService.cancelOrder(id);
return ResponseEntity.noContent().build();
}
}3.2 案例二:错误响应标准化
// 统一错误响应格式
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order with id '123' not found",
"details": [
{
"field": "orderId",
"message": "Order does not exist",
"rejected_value": "123"
}
],
"timestamp": "2024-01-15T10:30:00Z",
"trace_id": "abc-123-def"
}
}
// 业务校验错误
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": [
{
"field": "items",
"message": "Order must contain at least one item"
},
{
"field": "deliveryAddress",
"message": "Delivery address is required"
}
]
}
}四、本章小结
本章深入讲解了微服务中的 REST API 通信:
核心知识点:
- REST 资源建模遵循"名词用 URI、动词用 HTTP 方法"原则。
- 幂等性设计是微服务容错的基础,POST 请求需要 Idempotency-Key 或业务唯一键。
- API 版本管理推荐 URI 路径版本(内部)或 Header 版本(外部)。
- 容错机制(重试、熔断器)是微服务通信的必备组件。
- gRPC 适合内部高性能通信,REST 适合通用场景。
五、思考与练习
思考题
如何判断一个 API 变更是否是"向后兼容"的?
幂等性设计中,Idempotency-Key 的过期时间应该如何确定?
熔断器和重试机制的组合使用需要注意什么问题?
练习题
为一个库存服务设计完整的 REST API,包含资源建模、HTTP 方法、状态码。
实现一个带 Idempotency-Key 的 POST API。
使用 Resilience4j 配置重试和熔断器,模拟下游服务故障场景。