远程服务调用
远程服务调用让微服务间像调用本地方法一样通信。核心问题:用什么协议传输(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);
}超时与重试
远程调用可靠性三大件:超时 → 重试 → 幂等。
超时设置
| 场景 | 建议超时 | 说明 |
|---|---|---|
| 普通查询接口 | 3s | 连接超时 1s + 读取超时 2s |
| 写操作/事务接口 | 5-10s | 涉及 DB 操作、分布式事务 |
| 第三方支付/外部服务 | 15-30s | 外部不可控,但必须设上限 |
| 异步任务 | 不设读超时 | 交给任务框架管理,避免线程挂死 |
超时值不是越大越好:过长会积压线程资源,过短会误伤慢查询。线上通过 P99 延迟 + 3-5 倍系数设定。
重试策略
- 只对幂等操作重试:GET/PUT/DELETE 可重试,POST 必须在携带幂等 Token 时可重试
- 退避算法:固定退避(等 1s 重试)、指数退避(1s→2s→4s→8s)、抖动退避(指数退避 + 随机化,避免重试风暴)
- 重试次数上限:一般 2-3 次,超过上限直接降级
- 避免重试风暴:多个调用方同时失败同时重试会放大流量,重试应加随机抖动,配合 流量治理 的熔断器(熔断打开后停止重试)
指数退避 + 抖动示例:
第1次失败 → 等 1s±0.3s → 第2次失败 → 等 2s±0.6s → 第3次失败 → 放弃/降级序列化对比
| 格式 | 大小 | 速度 | 可读性 |
|---|---|---|---|
| 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 的学习成本和运维成本更高。
Q: 超时、重试、熔断怎么配合使用? A: 超时是第一道防线(每个调用必须有),重试是超时后的补救(仅限幂等操作,指数退避),熔断是重试之上的保险(失败率达到阈值后快速失败,停止无效重试)。顺序:请求 → 超时 → 重试(退避)→ 熔断打开 → 降级返回。只设超时不重试会丢失可用性,只重试不熔断会放大故障,三者缺一不可。