4. gRPC 四种通信模式:一元RPC、服务端流式RPC、客户端流式RPC、双向流式RPC
gRPC 基于 HTTP/2 协议,提供了四种通信模式,覆盖从简单的请求-响应到复杂的实时双向数据流场景。理解这四种模式是构建分布式缓存一致性系统的核心基础,不同的缓存同步策略会依赖不同的通信模式来实现。
4.1 四种模式概览
| 模式 | 通信方向 | 典型场景 | 缓存一致性应用 |
|---|---|---|---|
| 一元 RPC | 客户端 → 服务端 → 客户端 | 传统请求/响应、查询操作 | 缓存键查询、单条缓存失效通知 |
| 服务端流式 RPC | 客户端 → 服务端 → 客户端(流) | 服务端推送、批量数据下发 | 缓存预热、全量缓存同步 |
| 客户端流式 RPC | 客户端(流)→ 服务端 → 客户端 | 批量上传、日志聚合 | 批量缓存更新请求、缓存写入合并 |
| 双向流式 RPC | 客户端(流)↔ 服务端(流) | 实时聊天、实时协同 | 缓存订阅/发布、实时缓存状态同步 |
核心认知: 在缓存一致性场景中,一元 RPC 适合低频的缓存查询和失效操作;双向流式 RPC 则是实现实时缓存订阅与增量同步的最优选择。
4.2 一元 RPC(Unary RPC)
一元 RPC 是最基础的通信模式,客户端发送一个请求,服务端返回一个响应。其行为与传统的 HTTP REST 接口类似,但基于 Protobuf 序列化和 HTTP/2 传输,性能更高。
// proto 定义
service CacheService {
rpc GetCache(GetCacheRequest) returns (GetCacheResponse);
}
message GetCacheRequest {
string key = 1;
}
message GetCacheResponse {
bytes value = 1;
bool hit = 2;
}
执行流程:
- 客户端构造
GetCacheRequest消息,调用GetCache方法。 - gRPC 客户端库将请求序列化为 Protobuf 二进制,通过 HTTP/2 发送。
- 服务端接收、反序列化,执行缓存查询逻辑。
- 服务端构造
GetCacheResponse,序列化后返回给客户端。 - 客户端收到响应,完成一次 RPC 调用。
注意: 一元 RPC 在缓存一致性中适用于“查询后校验”场景,例如客户端先读取本地缓存,若未命中则通过一元 RPC 向中心缓存服务发起查询。但若用于缓存失效通知,则存在时效性不足的问题,因为每次通知都需要客户端主动发起请求。
4.3 服务端流式 RPC(Server Streaming RPC)
服务端流式 RPC 允许客户端发送一个请求,服务端返回一个消息流。客户端通过迭代读取流中的多个消息,直到流结束。这种模式非常适合服务端向客户端推送大量数据。
// proto 定义
service CacheSyncService {
rpc WarmUpCache(WarmUpRequest) returns (stream CacheEntry);
}
message WarmUpRequest {
string region = 1;
}
message CacheEntry {
string key = 1;
bytes value = 2;
int64 ttl = 3;
}
典型应用——缓存预热:
- 新节点加入缓存集群时,向中心服务发送
WarmUpRequest。 - 服务端持续推送该区域的所有缓存条目,客户端逐个接收并写入本地缓存。
- 流结束后,客户端本地缓存与中心服务保持一致。
优势: 相比一元 RPC 的多次请求,服务端流式 RPC 只需一次请求即可批量接收数据,大幅降低网络开销和延迟,适用于全量缓存同步场景。
4.4 客户端流式 RPC(Client Streaming RPC)
客户端流式 RPC 允许客户端发送一个消息流,服务端在接收完所有消息后返回一个响应。这种模式适用于客户端需要批量提交数据的场景。
// proto 定义
service CacheUpdateService {
rpc BatchUpdateCache(stream CacheUpdateRequest) returns (BatchUpdateResponse);
}
message CacheUpdateRequest {
string key = 1;
bytes value = 2;
Operation op = 3; // SET, DELETE
}
message BatchUpdateResponse {
int32 success_count = 1;
int32 fail_count = 2;
}
执行流程:
- 客户端打开一个流,开始发送多个
CacheUpdateRequest消息。 - 服务端持续接收消息,可以边接收边处理,也可以全部接收后统一处理。
- 客户端发送完毕,关闭流的写入端。
- 服务端处理完所有请求后,返回一个
BatchUpdateResponse。 - 客户端收到响应,完成本次批量操作。
缓存一致性应用: 当客户端本地缓存发生大量更新时,可以将更新请求合并为一个流,批量发送给中心缓存服务进行确认和同步,减少频繁的网络连接建立开销。
4.5 双向流式 RPC(Bidirectional Streaming RPC)
双向流式 RPC 允许客户端和服务端各自独立地发送和接收消息流。双方可以同时读写,实现全双工通信。这是四种模式中最灵活、最强大的模式,也是实现实时缓存一致性的核心。
// proto 定义
service CacheReplicationService {
rpc SyncCache(stream CacheEvent) returns (stream CacheAck);
}
message CacheEvent {
string key = 1;
EventType type = 2; // UPDATE, DELETE, INVALIDATE
bytes value = 3;
int64 version = 4;
}
message CacheAck {
string key = 1;
bool success = 2;
int64 applied_version = 3;
}
实时缓存订阅机制:
- 客户端建立双向流,发送订阅请求(如订阅特定键前缀的变更事件)。
- 服务端持续推送缓存变更事件(
CacheEvent)给客户端。 - 客户端处理事件后,返回确认消息(
CacheAck),包含应用后的版本号。 - 服务端根据确认情况决定是否需要重传,实现可靠的消息投递。
关键特性: 双向流式 RPC 的读写是独立的,客户端可以在接收服务端推送的同时,发送自己的缓存更新请求。这种异步非阻塞的通信模式,使得缓存一致性的延迟可以降低到毫秒级。
4.6 四种模式对比与选择
| 维度 | 一元 RPC | 服务端流式 | 客户端流式 | 双向流式 |
|---|---|---|---|---|
| 消息模型 | 1 请求 → 1 响应 | 1 请求 → N 响应 | N 请求 → 1 响应 | N 请求 ↔ N 响应 |
| 实时性 | 低(需轮询) | 中(服务端主动推送) | 低(批量提交) | 高(全双工实时) |
| 资源开销 | 低 | 中(需维护流) | 中(需维护流) | 高(长连接+双流) |
| 缓存一致性场景 | 缓存查询、单条失效 | 缓存预热、全量同步 | 批量更新、写入合并 | 实时订阅、增量同步 |
| 实现复杂度 | 低 | 中 | 中 | 高(需处理并发读写) |
选择建议: 在构建缓存一致性系统时,建议采用“分层混合”策略——使用一元 RPC 处理常规查询,使用服务端流式 RPC 处理节点启动时的全量同步,使用双向流式 RPC 处理运行期间的实时增量同步。避免在单一连接上混合使用多种模式,应针对不同场景建立独立的 gRPC 连接。
4.7 四种模式通信流程对比
下图展示了四种 gRPC 通信模式的核心交互流程,帮助理解客户端与服务端之间的消息传递方式。
4.7 代码示例:双向流式 RPC 实现缓存订阅
以下代码展示了如何使用 Go 实现双向流式 RPC 的缓存订阅服务端和客户端核心逻辑。
服务端实现:
// 服务端:处理缓存订阅的双向流
func (s *CacheReplicationServer) SyncCache(stream pb.CacheReplicationService_SyncCacheServer) error {
// 接收客户端的订阅请求
req, err := stream.Recv()
if err != nil {
return err
}
// 注册客户端到订阅管理器
subscriber := &Subscriber{
stream: stream,
events: make(chan *pb.CacheEvent, 100),
}
s.subManager.Register(req.KeyPrefix, subscriber)
defer s.subManager.Unregister(req.KeyPrefix, subscriber)
// 启动 goroutine 处理服务端到客户端的推送
go func() {
for event := range subscriber.events {
if err := stream.Send(event); err != nil {
// 发送失败,关闭连接
return
}
}
}()
// 主循环:接收客户端的确认消息
for {
ack, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// 处理确认,更新重试队列
s.ackManager.ProcessAck(ack)
}
}
客户端实现:
// 客户端:建立双向流并处理缓存事件
func (c *CacheClient) Subscribe(prefix string) error {
stream, err := c.client.SyncCache(context.Background())
if err != nil {
return err
}
// 发送订阅请求
if err := stream.Send(&pb.CacheEvent{
KeyPrefix: prefix,
Type: pb.EventType_SUBSCRIBE,
}); err != nil {
return err
}
// 启动 goroutine 处理服务端推送的事件
go func() {
for {
event, err := stream.Recv()
if err == io.EOF {
return
}
if err != nil {
log.Printf("接收事件失败: %v", err)
return
}
// 应用缓存变更
c.applyCacheEvent(event)
// 发送确认
stream.Send(&pb.CacheAck{
Key: event.Key,
Success: true,
AppliedVersion: event.Version,
})
}
}()
return nil
}
实践要点: 双向流式 RPC 中,服务端和客户端的
Recv 和 Send 是独立的 goroutine 安全操作。建议为读写分别使用独立的 goroutine,避免相互阻塞。同时,需要实现心跳机制和重连逻辑,以应对网络波动导致的流中断。
4.8 总结
gRPC 的四种通信模式为缓存一致性系统提供了灵活的选择:
- 一元 RPC 适合简单的查询和单条操作,实现简单但实时性不足。
- 服务端流式 RPC 适合服务端主动推送大量数据,如缓存预热。
- 客户端流式 RPC 适合客户端批量提交数据,如批量缓存更新。
- 双向流式 RPC 是实现实时缓存一致性的核心模式,支持全双工通信和增量同步。
在实际项目中,应根据缓存一致性的具体需求(实时性要求、数据量大小、网络条件)选择合适的模式,或组合使用多种模式构建分层缓存同步架构。