141 lines
5.4 KiB
Markdown
141 lines
5.4 KiB
Markdown
---
|
||
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]]
|