远程服务调用
远程服务调用让微服务间像调用本地方法一样通信。核心问题:用什么协议传输(REST/RPC)、用什么格式序列化(JSON/Protobuf)、如何保证可靠性(超时/重试/幂等)。
提示
REST、RPC、gRPC 等远程调用协议与设计规范
REST vs RPC
| 维度 | REST | RPC |
|---|---|---|
| 抽象目标 | 面向资源(名词) | 面向方法(动词) |
| 表达方式 | URL 是资源,HTTP 动词是操作 | 直接调用远程方法 |
| 协议 | HTTP | TCP/HTTP/二进制 |
| 适用场景 | 外部 API、跨平台 | 系统内部高性能通信 |
REST:GET /users/123(获取 123 号用户) RPC:userService.getUserById(123)(调用远程方法)
Richardson 成熟度模型
| Level | 特征 |
|---|---|
| Level 0 | 单一 URL,所有操作指向同一端点,本质是 SOAP |
| Level 1 | 每个资源有独立 URI,但操作混在请求体 |
| Level 2 | 充分利用 GET/POST/PUT/DELETE 语义 |
| Level 3 | 响应中携带下一步操作的链接(HATEOAS) |
工程实践中 Level 2 是大多数项目的目标。
gRPC
gRPC 是 Google 开源的高性能 RPC 框架,三大核心优势:
| 优势 | 说明 |
|---|---|
| Protobuf | 二进制格式,体积比 JSON 小 3-10 倍 |
| HTTP/2 | 多路复用、头部压缩、双向流 |
| 多语言代码生成 | .proto 定义接口,自动生成各语言代码 |
四种调用模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Unary | 一请求一响应 | 普通 API |
| Server Streaming | 一请求流响应 | 实时推送 |
| Client Streaming | 流请求一响应 | 文件上传 |
| Bidirectional Streaming | 双向流 | 即时通讯 |
OpenFeign vs Dubbo
| 维度 | OpenFeign | Dubbo |
|---|---|---|
| 协议 | HTTP 短连接 | TCP 长连接 + 自定义协议 |
| 序列化 | JSON | Hessian/Kryo 二进制 |
| 性能 | 较低 | 高 |
| 服务治理 | 依赖 服务治理 组件 | 内置 |
| 适用场景 | 中小型微服务 | 大型分布式系统 |
@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceClient {
@GetMapping("/users/{id}")
User getUserById(@PathVariable("id") Long id);
}幂等性设计
分布式场景下超时重试可能触发重复请求,必须保证幂等。分布式事务 中 SAGA 模式的补偿操作也依赖幂等性。
HTTP 方法的幂等性
| 方法 | 幂等 | 说明 |
|---|---|---|
| GET | 是 | 只读,无副作用 |
| DELETE | 是 | 删除相同资源结果一致 |
| PUT | 是 | 更新/创建,重复请求结果一致 |
| POST | 否 | 重复提交可能创建多个资源 |
分布式幂等实现
核心思路:为每次请求生成唯一标识,通过持久化记录判断是否重复执行。
| 方案 | 说明 | 适用场景 |
|---|---|---|
| 数据库唯一约束 | 业务字段加唯一索引 | 防重复下单 |
| Redis SetNX | 原子性「设置并检查」 | 接口幂等 Token |
| 乐观锁 | UPDATE ... WHERE version = ? | 库存扣减 |
| 状态机 | 只允许特定状态转换 | 订单状态变更 |
@PostMapping("/orders")
public Order createOrder(@RequestHeader("X-Idempotency-Token") String token,
@RequestBody CreateOrderDTO dto) {
// SetNX 原子操作:仅当 key 不存在时才设置成功,保证同一 token 只处理一次
Boolean firstRequest = redis.setNX("idem:" + token, "1", 5, TimeUnit.MINUTES);
if (!firstRequest) throw new DuplicateRequestException();
return orderService.create(dto);
}序列化对比
| 格式 | 大小 | 速度 | 可读性 |
|---|---|---|---|
| JSON | 大 | 中 | 是 |
| Protobuf | 最小 | 最快 | 否(代码生成) |
| Hessian | 小 | 快 | 否 |
外部 API → JSON(可读性优先),内部微服务 → Protobuf(性能优先)。
适用场景
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 对外开放 API | REST + JSON | 跨平台、可读性好、生态成熟 |
| 内部高性能通信 | gRPC + Protobuf | 二进制序列化、HTTP/2 多路复用 |
| Spring Cloud 微服务 | OpenFeign | 声明式调用、与 Spring 生态无缝集成 |
| 大型分布式系统 | Dubbo | TCP 长连接、内置服务治理能力 |
FAQ
Q: REST 和 RPC 该选哪个? A: 对外 API 选 REST(跨平台、可读性好),内部服务间通信选 RPC(性能高、代码生成方便)。如果团队技术栈统一且对性能有要求,内部全部用 gRPC;如果需要兼容多语言或对外暴露,用 REST。
Q: 幂等性设计是必须的吗? A: 在分布式场景下是必须的。网络超时、重试机制、消息重复投递都可能导致重复请求。GET/PUT/DELETE 天然幂等,POST 需要通过唯一约束、Redis SetNX、乐观锁、状态机等方案保证幂等。不设计幂等性,重复请求可能导致数据不一致。
Q: OpenFeign 和 Dubbo 如何选择? A: 中小型微服务、团队熟悉 Spring 生态选 OpenFeign(开发简单、HTTP 短连接);大型分布式系统、对性能有极致要求选 Dubbo(TCP 长连接、二进制序列化、内置负载均衡和熔断)。Dubbo 的学习成本和运维成本更高。