|
| 1 | +# RocketMQ Request-Reply 使用指南 |
| 2 | + |
| 3 | +## 概述 |
| 4 | + |
| 5 | +从 RocketMQ 4.6.0 版本开始,引入了 Request-Reply 特性,该特性允许生产者在发送消息后同步或异步等待消费者消费完消息并返回响应消息,实现类似 RPC 调用的效果。 |
| 6 | + |
| 7 | +NewLife.RocketMQ 现已支持此特性,并兼容 RocketMQ 5.0+。 |
| 8 | + |
| 9 | +## 主要特性 |
| 10 | + |
| 11 | +- **同步请求**:发送请求后阻塞等待响应 |
| 12 | +- **异步请求**:发送请求后异步等待响应 |
| 13 | +- **超时控制**:支持设置请求超时时间 |
| 14 | +- **自动关联**:自动管理请求和响应的关联 |
| 15 | +- **简单易用**:API 设计简洁,易于集成 |
| 16 | + |
| 17 | +## 使用示例 |
| 18 | + |
| 19 | +### 1. 生产者端 - 发送请求 |
| 20 | + |
| 21 | +#### 同步请求 |
| 22 | + |
| 23 | +```csharp |
| 24 | +using NewLife.RocketMQ; |
| 25 | + |
| 26 | +// 创建生产者 |
| 27 | +var producer = new Producer |
| 28 | +{ |
| 29 | + Topic = "request_topic", |
| 30 | + NameServerAddress = "127.0.0.1:9876", |
| 31 | + RequestTimeout = 3000 // 设置默认超时时间为3秒 |
| 32 | +}; |
| 33 | +producer.Start(); |
| 34 | + |
| 35 | +try |
| 36 | +{ |
| 37 | + // 发送请求并同步等待响应 |
| 38 | + var requestBody = "这是一个请求消息"; |
| 39 | + var response = producer.Request(requestBody, timeout: 5000); |
| 40 | + |
| 41 | + Console.WriteLine($"收到响应: {response.BodyString}"); |
| 42 | +} |
| 43 | +finally |
| 44 | +{ |
| 45 | + producer.Stop(); |
| 46 | + producer.Dispose(); |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +#### 异步请求 |
| 51 | + |
| 52 | +```csharp |
| 53 | +using NewLife.RocketMQ; |
| 54 | + |
| 55 | +// 创建生产者 |
| 56 | +var producer = new Producer |
| 57 | +{ |
| 58 | + Topic = "request_topic", |
| 59 | + NameServerAddress = "127.0.0.1:9876", |
| 60 | + RequestTimeout = 3000 |
| 61 | +}; |
| 62 | +producer.Start(); |
| 63 | + |
| 64 | +try |
| 65 | +{ |
| 66 | + // 异步发送请求并等待响应 |
| 67 | + var requestBody = "这是一个异步请求消息"; |
| 68 | + var response = await producer.RequestAsync(requestBody, timeout: 5000); |
| 69 | + |
| 70 | + Console.WriteLine($"收到响应: {response.BodyString}"); |
| 71 | +} |
| 72 | +finally |
| 73 | +{ |
| 74 | + producer.Stop(); |
| 75 | + producer.Dispose(); |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +### 2. 消费者端 - 处理请求并发送回复 |
| 80 | + |
| 81 | +#### 同步处理 |
| 82 | + |
| 83 | +```csharp |
| 84 | +using NewLife.RocketMQ; |
| 85 | + |
| 86 | +// 创建消费者 |
| 87 | +var consumer = new Consumer |
| 88 | +{ |
| 89 | + Topic = "request_topic", |
| 90 | + Group = "request_consumer_group", |
| 91 | + NameServerAddress = "127.0.0.1:9876", |
| 92 | + FromLastOffset = false |
| 93 | +}; |
| 94 | + |
| 95 | +// 设置消息处理回调 |
| 96 | +consumer.OnConsume = (queue, messages) => |
| 97 | +{ |
| 98 | + foreach (var message in messages) |
| 99 | + { |
| 100 | + Console.WriteLine($"收到请求: {message.BodyString}"); |
| 101 | + |
| 102 | + // 检查是否是请求消息 |
| 103 | + if (!String.IsNullOrEmpty(message.CorrelationId)) |
| 104 | + { |
| 105 | + // 处理业务逻辑 |
| 106 | + var result = ProcessRequest(message.BodyString); |
| 107 | + |
| 108 | + // 发送回复 |
| 109 | + consumer.SendReply(message, result); |
| 110 | + |
| 111 | + Console.WriteLine($"已发送回复: {result}"); |
| 112 | + } |
| 113 | + } |
| 114 | + return true; |
| 115 | +}; |
| 116 | + |
| 117 | +consumer.Start(); |
| 118 | + |
| 119 | +// 保持运行 |
| 120 | +Console.WriteLine("消费者已启动,按任意键退出..."); |
| 121 | +Console.ReadKey(); |
| 122 | + |
| 123 | +consumer.Stop(); |
| 124 | +consumer.Dispose(); |
| 125 | + |
| 126 | +string ProcessRequest(string request) |
| 127 | +{ |
| 128 | + // 实现你的业务逻辑 |
| 129 | + return $"处理结果: {request}"; |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +#### 异步处理 |
| 134 | + |
| 135 | +```csharp |
| 136 | +using NewLife.RocketMQ; |
| 137 | + |
| 138 | +// 创建消费者 |
| 139 | +var consumer = new Consumer |
| 140 | +{ |
| 141 | + Topic = "request_topic", |
| 142 | + Group = "request_consumer_group", |
| 143 | + NameServerAddress = "127.0.0.1:9876", |
| 144 | + FromLastOffset = false |
| 145 | +}; |
| 146 | + |
| 147 | +// 设置异步消息处理回调 |
| 148 | +consumer.OnConsumeAsync = async (queue, messages, cancellationToken) => |
| 149 | +{ |
| 150 | + foreach (var message in messages) |
| 151 | + { |
| 152 | + Console.WriteLine($"收到请求: {message.BodyString}"); |
| 153 | + |
| 154 | + // 检查是否是请求消息 |
| 155 | + if (!String.IsNullOrEmpty(message.CorrelationId)) |
| 156 | + { |
| 157 | + // 异步处理业务逻辑 |
| 158 | + var result = await ProcessRequestAsync(message.BodyString, cancellationToken); |
| 159 | + |
| 160 | + // 异步发送回复 |
| 161 | + await consumer.SendReplyAsync(message, result, cancellationToken); |
| 162 | + |
| 163 | + Console.WriteLine($"已发送回复: {result}"); |
| 164 | + } |
| 165 | + } |
| 166 | + return true; |
| 167 | +}; |
| 168 | + |
| 169 | +consumer.Start(); |
| 170 | + |
| 171 | +// 保持运行 |
| 172 | +Console.WriteLine("消费者已启动,按任意键退出..."); |
| 173 | +Console.ReadKey(); |
| 174 | + |
| 175 | +consumer.Stop(); |
| 176 | +consumer.Dispose(); |
| 177 | + |
| 178 | +async Task<string> ProcessRequestAsync(string request, CancellationToken ct) |
| 179 | +{ |
| 180 | + // 实现你的异步业务逻辑 |
| 181 | + await Task.Delay(100, ct); // 模拟异步操作 |
| 182 | + return $"处理结果: {request}"; |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +### 3. 超时处理 |
| 187 | + |
| 188 | +```csharp |
| 189 | +using NewLife.RocketMQ; |
| 190 | + |
| 191 | +var producer = new Producer |
| 192 | +{ |
| 193 | + Topic = "request_topic", |
| 194 | + NameServerAddress = "127.0.0.1:9876", |
| 195 | + RequestTimeout = 1000 // 默认超时1秒 |
| 196 | +}; |
| 197 | +producer.Start(); |
| 198 | + |
| 199 | +try |
| 200 | +{ |
| 201 | + var response = await producer.RequestAsync("请求消息"); |
| 202 | + Console.WriteLine($"收到响应: {response.BodyString}"); |
| 203 | +} |
| 204 | +catch (TimeoutException ex) |
| 205 | +{ |
| 206 | + Console.WriteLine($"请求超时: {ex.Message}"); |
| 207 | +} |
| 208 | +finally |
| 209 | +{ |
| 210 | + producer.Stop(); |
| 211 | + producer.Dispose(); |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +## API 参考 |
| 216 | + |
| 217 | +### Producer 类 |
| 218 | + |
| 219 | +#### 属性 |
| 220 | + |
| 221 | +- `RequestTimeout`:请求超时时间(毫秒),默认 3000ms |
| 222 | + |
| 223 | +#### 方法 |
| 224 | + |
| 225 | +- `MessageExt Request(Message message, Int32 timeout = -1)` |
| 226 | + - 发送请求消息,同步等待响应 |
| 227 | + - 参数: |
| 228 | + - `message`:请求消息 |
| 229 | + - `timeout`:超时时间(毫秒),-1 表示使用默认超时时间 |
| 230 | + - 返回:响应消息 |
| 231 | + - 异常:`TimeoutException` - 请求超时 |
| 232 | + |
| 233 | +- `MessageExt Request(Object body, Int32 timeout = -1)` |
| 234 | + - 发送请求消息,同步等待响应(简化版本) |
| 235 | + - 参数: |
| 236 | + - `body`:消息体内容 |
| 237 | + - `timeout`:超时时间(毫秒) |
| 238 | + - 返回:响应消息 |
| 239 | + |
| 240 | +- `Task<MessageExt> RequestAsync(Message message, Int32 timeout = -1, CancellationToken cancellationToken = default)` |
| 241 | + - 异步发送请求消息并等待响应 |
| 242 | + - 参数: |
| 243 | + - `message`:请求消息 |
| 244 | + - `timeout`:超时时间(毫秒) |
| 245 | + - `cancellationToken`:取消令牌 |
| 246 | + - 返回:响应消息 |
| 247 | + - 异常:`TimeoutException` - 请求超时 |
| 248 | + |
| 249 | +- `Task<MessageExt> RequestAsync(Object body, Int32 timeout = -1, CancellationToken cancellationToken = default)` |
| 250 | + - 异步发送请求消息并等待响应(简化版本) |
| 251 | + |
| 252 | +### Consumer 类 |
| 253 | + |
| 254 | +#### 方法 |
| 255 | + |
| 256 | +- `SendResult SendReply(MessageExt requestMessage, Object replyBody)` |
| 257 | + - 发送回复消息 |
| 258 | + - 参数: |
| 259 | + - `requestMessage`:原始请求消息 |
| 260 | + - `replyBody`:回复消息内容 |
| 261 | + - 返回:发送结果 |
| 262 | + - 异常: |
| 263 | + - `ArgumentNullException` - 参数为空 |
| 264 | + - `InvalidOperationException` - 请求消息缺少必要属性 |
| 265 | + |
| 266 | +- `Task<SendResult> SendReplyAsync(MessageExt requestMessage, Object replyBody, CancellationToken cancellationToken = default)` |
| 267 | + - 异步发送回复消息 |
| 268 | + - 参数: |
| 269 | + - `requestMessage`:原始请求消息 |
| 270 | + - `replyBody`:回复消息内容 |
| 271 | + - `cancellationToken`:取消令牌 |
| 272 | + - 返回:发送结果 |
| 273 | + |
| 274 | +### Message 类新增属性 |
| 275 | + |
| 276 | +- `ReplyToClient`:回复地址,指示回复消息应发送到的客户端ID |
| 277 | +- `CorrelationId`:关联ID,用于将回复消息与请求消息关联 |
| 278 | +- `MessageType`:消息类型,用于区分普通消息和回复消息("REQUEST"/"REPLY") |
| 279 | +- `RequestTimeout`:请求超时时间(毫秒) |
| 280 | + |
| 281 | +## 注意事项 |
| 282 | + |
| 283 | +1. **版本要求**:需要 RocketMQ 服务器版本 4.6.0 或更高 |
| 284 | +2. **超时设置**:合理设置超时时间,避免长时间阻塞 |
| 285 | +3. **异常处理**:务必捕获 `TimeoutException` 处理超时情况 |
| 286 | +4. **资源释放**:使用完毕后及时释放 Producer 和 Consumer 资源 |
| 287 | +5. **Topic 规划**:建议为 Request-Reply 使用独立的 Topic |
| 288 | +6. **消费者处理**:消费者必须检查 `CorrelationId` 属性来判断是否为请求消息 |
| 289 | + |
| 290 | +## 兼容性 |
| 291 | + |
| 292 | +- 支持 .NET Framework 4.5+ |
| 293 | +- 支持 .NET Standard 2.0+ |
| 294 | +- 支持 .NET Core 2.0+ |
| 295 | +- 支持 .NET 5.0+ |
| 296 | +- 兼容 RocketMQ 4.6.0 或以上版本 |
| 297 | +- 兼容 RocketMQ 5.0 或以上版本 |
| 298 | + |
| 299 | +## 性能建议 |
| 300 | + |
| 301 | +1. 复用 Producer 和 Consumer 实例,避免频繁创建销毁 |
| 302 | +2. 合理设置超时时间,避免资源浪费 |
| 303 | +3. 对于高并发场景,建议使用异步 API |
| 304 | +4. 监控回复消息的处理时间,及时优化业务逻辑 |
| 305 | + |
| 306 | +## 故障排查 |
| 307 | + |
| 308 | +### 请求超时 |
| 309 | + |
| 310 | +- 检查消费者是否正常运行 |
| 311 | +- 检查网络连接是否正常 |
| 312 | +- 检查消费者处理逻辑是否耗时过长 |
| 313 | +- 适当增加超时时间 |
| 314 | + |
| 315 | +### 收不到回复 |
| 316 | + |
| 317 | +- 确认消费者正确调用了 `SendReply` 或 `SendReplyAsync` |
| 318 | +- 检查消费者日志,确认是否有异常 |
| 319 | +- 确认消息的 `CorrelationId` 属性正确设置 |
| 320 | +- 检查 Topic 配置是否正确 |
| 321 | + |
| 322 | +## 更多示例 |
| 323 | + |
| 324 | +更多使用示例请参考项目源码中的单元测试:`XUnitTestRocketMQ/RequestReplyTests.cs` |
0 commit comments