3. 微服务通信基础
微服务拆开之后,服务之间必须通信。通信方式选得对不对,直接决定系统的延迟、吞吐、可靠性和演进成本。本节先厘清两条主线:协议层是选 HTTP/REST 还是 gRPC,交互层是选同步请求还是异步消息;再落到 ASP.NET Core 中 HttpClient 与 IHttpClientFactory 的正确用法。
一、HTTP/REST 与 gRPC:不是替代,而是分工
HTTP/REST 以资源为中心,用 URL 定位资源、用 HTTP 动词表达操作、用 JSON 承载数据。它的最大优势是通用:浏览器、curl、Postman、任何语言都能直接调用,调试成本极低。代价是文本序列化体积大、每次请求都要携带完整头部、没有强类型契约,字段改名容易在运行时才暴露。
gRPC 以方法为中心,基于 HTTP/2 传输,用 Protocol Buffers 定义服务契约并二进制序列化。它天然支持多路复用、双向流、强类型代码生成,适合内部服务之间高频、低延迟、结构稳定的调用。代价是浏览器不能直接调用(需 gRPC-Web 代理)、二进制报文不便肉眼排查、契约变更需要重新生成代码。
选择时可以按下面的判断走:面向外部或前端的 API 用 REST;服务内部高频调用、需要流式传输或严格契约的用 gRPC;同一服务也可以同时暴露两套接口,对外 REST、对内 gRPC。
二、同步通信与异步消息:可靠性与耦合度的取舍
同步通信指调用方发出请求后阻塞等待响应,REST 和 gRPC 的一元调用都属于这一类。它逻辑直观、结果即时,但调用链上任何一环变慢或宕机都会向上传导,形成级联故障。同步调用适合“必须立刻拿到结果才能继续”的场景,例如下单前校验库存。
异步消息指调用方把消息投递到消息中间件(RabbitMQ、Kafka、Azure Service Bus 等)后立即返回,由消费者按自己的节奏处理。它解耦了生产者和消费者,天然具备削峰填谷和失败重试能力,代价是最终一致性、消息幂等、顺序与重复消费都需要额外设计。异步适合“可以稍后处理”的场景,例如下单成功后发短信、更新积分、写审计日志。
工程上常见的是两者混用:核心链路用同步保证强一致,旁路动作走异步消息。判断标准很简单——调用方是否必须立刻得到结果;如果不需要,就应优先异步。
三、HttpClient 与 IHttpClientFactory 的正确用法
在 ASP.NET Core 中直接 new HttpClient() 是典型反模式:每次新建都会创建新的连接池,频繁调用会耗尽套接字(socket exhaustion);而长期持有单个静态实例又会导致 DNS 变更无法生效。IHttpClientFactory 正是为解决这两个矛盾而设计,它统一管理底层 HttpMessageHandler 的生命周期与连接池复用。
推荐做法是在 Program.cs 中注册具名客户端或类型化客户端,并配置基地址、超时和重试策略:
builder.Services.AddHttpClient("inventory", client =>
{
client.BaseAddress = new Uri("https://inventory.internal/");
client.Timeout = TimeSpan.FromSeconds(3);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddStandardResilienceHandler(); // 内置重试、熔断、超时
// 类型化客户端:把 HTTP 细节封装在服务类里
builder.Services.AddHttpClient<IInventoryClient, InventoryClient>(client =>
{
client.BaseAddress = new Uri("https://inventory.internal/");
client.Timeout = TimeSpan.FromSeconds(3);
});
消费时通过构造函数注入 IHttpClientFactory,用 CreateClient("inventory") 获取实例。注意:HttpClient 实例应当短生命周期使用,用完即弃,底层 Handler 由工厂池化复用,不要把它存进字段长期持有。
public class OrderService
{
private readonly IHttpClientFactory _factory;
public OrderService(IHttpClientFactory factory) => _factory = factory;
public async Task<StockResult> CheckStockAsync(string sku, int qty, CancellationToken ct)
{
var client = _factory.CreateClient("inventory");
var resp = await client.GetAsync($"/api/stock/{sku}?qty={qty}", ct);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadFromJsonAsync<StockResult>(ct);
}
}
几个容易踩的坑:一是超时不要依赖默认的 100 秒,内部调用通常应设为 1~3 秒并配合取消令牌;二是重试只对幂等的 GET 或带幂等键的请求开启,否则可能重复下单;三是不要在 AddHttpClient 里做耗时初始化,配置委托应保持轻量;四是跨服务传递 CancellationToken,让上游取消能及时释放下游资源。
把这三层决策串起来看:协议决定报文长什么样,交互模式决定调用方是否等待,客户端工厂决定连接资源如何被安全复用。三者配合得当,微服务之间的通信才既高效又可控。