--- tags: - MQ create time: 2026-05-24 19:52 --- # MQ 请求-回复模式 ## 概述 请求-回复模式(Request-Reply)是通过 MQ 实现同步 RPC 语义的方式:Producer 发送请求消息,Consumer 处理后将响应发送到 Reply-To 队列,Producer 通过 CorrelationID 匹配响应。这种模式在异步通道上模拟了同步调用,适用于跨网络不可靠、需要消息持久化或遗留系统集成的场景。 ## 正文 ### 基本原理 传统 RPC 是一问一答:客户端发请求,服务端返回响应。请求-回复模式把这个过程搬到了 MQ 上: 1. Producer 发送请求消息,携带 `ReplyTo`(回复队列名)和 `CorrelationID`(请求标识) 2. Consumer 从请求队列消费消息,处理业务逻辑 3. Consumer 将响应发送到 `ReplyTo` 队列,携带相同的 `CorrelationID` 4. Producer 从 ReplyTo 队列消费响应,用 `CorrelationID` 匹配请求 ```mermaid sequenceDiagram participant P as Producer participant BQ as "Broker Request Queue" participant C as Consumer participant RQ as "Broker Reply Queue" P->>BQ: "发送请求 (CorrelationID=abc123, ReplyTo=reply-queue)" BQ->>C: "投递请求" C->>C: "处理业务逻辑" C->>RQ: "发送响应 (CorrelationID=abc123)" RQ->>P: "投递响应" P->>P: "匹配 CorrelationID, 返回结果" ``` 关键在于 `CorrelationID`——它是请求和响应之间的"凭证"。没有它,当多个请求并发时,Producer 无法知道哪个响应对应哪个请求。 ### 与直接 RPC 的对比 | 维度 | gRPC / HTTP | MQ 请求-回复 | |------|------------|-------------| | 耦合度 | 直连,需要知道服务地址 | 通过 Broker 中转,天然解耦 | | 削峰能力 | 无,服务端过载直接拒绝 | Broker 缓冲,削峰填谷 | | 延迟 | 低(一次网络跳转) | 高(至少两次 Broker 中转) | | 持久化 | 无 | 消息可持久化,故障可恢复 | | 复杂度 | 低 | 高(CorrelationID 匹配、超时管理) | > [!question] > 请求-回复模式看起来像用 MQ 模拟 RPC,什么情况下这比直接用 gRPC 更好? 答案藏在"代价"里:你付出的是延迟和复杂度,换来的是解耦、削峰和可靠性。当这些特性比低延迟更重要时,请求-回复模式就是合理的选择。 ### 适用场景 **跨网络不可靠的远程调用**:网络不稳定时,MQ 的持久化和重试机制比直连更可靠。消息不会因为网络抖动而丢失。 **需要消息持久化的 RPC**:某些金融场景要求每一次请求都有据可查。MQ 天然支持消息持久化,可以作为审计日志。 **遗留系统集成**:老系统可能只暴露 MQ 接口(比如 IBM MQ),新系统通过请求-回复模式与其交互,避免大规模改造。 **异步长任务**:某些请求处理耗时很长(分钟级),用直连 RPC 会超时。通过 MQ,Producer 发完请求就可以去做别的,异步接收结果。 ### 注意事项 **超时处理**:Producer 不能无限等待回复。需要设置超时,超时后清理本地的 pending 请求映射。超时的响应如果后续到达,应该被丢弃或记录日志。 **Reply-To 队列的生命周期管理**: - **持久队列**:一个 Producer 固定使用一个回复队列,适合长期运行的服务 - **临时队列**:每次请求创建一个临时队列,用完即删。适合短生命周期的客户端,但创建和销毁队列有额外开销 **消息确认**:响应消息也需要 ACK 机制,否则 Broker 会反复投递,导致重复处理。 ### Go 实现 下面是通过 RabbitMQ 实现请求-回复模式的 Producer 和 Consumer 示例。 **Producer 端**:发送请求并等待回复 ```go func rpcClient(ch *amqp.Channel, request string) (string, error) { // 声明一个独占的临时回复队列 replyQ, err := ch.QueueDeclare("", false, false, true, false, nil) if err != nil { return "", err } corrID := uuid.New().String() // 发送请求,附带 ReplyTo 和 CorrelationID ch.PublishWithContext(ctx, "", "rpc_queue", false, false, amqp.Publishing{ ContentType: "text/plain", CorrelationId: corrID, ReplyTo: replyQ.Name, Body: []byte(request), }) // 等待匹配的回复 msgs, _ := ch.Consume(replyQ.Name, "", true, false, false, false, nil) for msg := range msgs { if msg.CorrelationId == corrID { return string(msg.Body), nil } } return "", fmt.Errorf("timeout") } ``` **Consumer 端**:处理请求并发送回复 ```go func rpcServer(ch *amqp.Channel) { msgs, _ := ch.Consume("rpc_queue", "", false, false, false, false, nil) for msg := range msgs { // 处理请求 result := processRequest(msg.Body) // 将回复发送到 ReplyTo 队列,携带相同的 CorrelationID ch.PublishWithContext(ctx, "", msg.ReplyTo, false, false, amqp.Publishing{ ContentType: "text/plain", CorrelationId: msg.CorrelationId, Body: []byte(result), }) msg.Ack(false) } } ``` Producer 用一个临时队列接收回复,通过 `CorrelationID` 精确匹配。Consumer 只需要把处理结果发回 `ReplyTo` 队列即可——这就是请求-回复模式的全部核心逻辑。 ## 关联笔记 - [[31-MQ-背压与流控]] - [[33-MQ-与流处理]] - [[34-事件驱动架构-EDA]]