Files
cs-note/hhs/MQ/08-消息设计模式/32-MQ-请求-回复模式.md
T
2026-05-24 20:51:06 +08:00

5.4 KiB
Raw Blame History

tags, create time
tags create time
MQ
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 匹配请求
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 端:发送请求并等待回复

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 端:处理请求并发送回复

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 队列即可——这就是请求-回复模式的全部核心逻辑。

关联笔记