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

141 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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]]