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;
}

执行流程:

  1. 客户端构造 GetCacheRequest 消息,调用 GetCache 方法。
  2. gRPC 客户端库将请求序列化为 Protobuf 二进制,通过 HTTP/2 发送。
  3. 服务端接收、反序列化,执行缓存查询逻辑。
  4. 服务端构造 GetCacheResponse,序列化后返回给客户端。
  5. 客户端收到响应,完成一次 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;
}

执行流程:

  1. 客户端打开一个流,开始发送多个 CacheUpdateRequest 消息。
  2. 服务端持续接收消息,可以边接收边处理,也可以全部接收后统一处理。
  3. 客户端发送完毕,关闭流的写入端。
  4. 服务端处理完所有请求后,返回一个 BatchUpdateResponse。
  5. 客户端收到响应,完成本次批量操作。
缓存一致性应用: 当客户端本地缓存发生大量更新时,可以将更新请求合并为一个流,批量发送给中心缓存服务进行确认和同步,减少频繁的网络连接建立开销。

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 通信模式的核心交互流程,帮助理解客户端与服务端之间的消息传递方式。

gRPC 四种通信模式流程对比 gRPC 四种通信模式流程对比 一元 RPC 客户端 服务端 请求 响应 服务端流式 RPC 客户端 服务端 请求 响应流 (N 条消息) 客户端流式 RPC 客户端 服务端 请求流 (N 条消息) 响应 双向流式 RPC 客户端 (读写独立) 服务端 (读写独立) 请求流 响应流 说明: 一元 RPC 为同步阻塞模式;流式 RPC 均为异步非阻塞模式。双向流式 RPC 的读写流完全独立,可实现全双工实时通信。

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 是实现实时缓存一致性的核心模式,支持全双工通信和增量同步。

在实际项目中,应根据缓存一致性的具体需求(实时性要求、数据量大小、网络条件)选择合适的模式,或组合使用多种模式构建分层缓存同步架构。