|
194 | 194 |
|
195 | 195 | 你可以看到图中生产者组中的生产者会向主题发送消息,而 **主题中存在多个队列**,生产者每次生产消息之后是指定主题中的某个队列发送消息的。
|
196 | 196 |
|
| 197 | + |
| 198 | + |
197 | 199 | 每个主题中都有多个队列(分布在不同的 `Broker`中,如果是集群的话,`Broker`又分布在不同的服务器中),集群消费模式下,一个消费者集群多台机器共同消费一个 `topic` 的多个队列,**一个队列只会被一个消费者消费**。如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。就像上图中 `Consumer1` 和 `Consumer2` 分别对应着两个队列,而 `Consumer3` 是没有队列对应的,所以一般来讲要控制 **消费者组中的消费者个数和主题中队列个数相同** 。
|
198 | 200 |
|
199 | 201 | 当然也可以消费者个数小于队列个数,只不过不太建议。如下图。
|
@@ -268,6 +270,168 @@ tag:
|
268 | 270 |
|
269 | 271 | 第四、消费者通过 `NameServer` 获取所有 `Broker` 的路由信息后,向 `Broker` 发送 `Pull` 请求来获取消息数据。`Consumer` 可以以两种模式启动—— **广播(Broadcast)和集群(Cluster)**。广播模式下,一条消息会发送给 **同一个消费组中的所有消费者** ,集群模式下消息只会发送给一个消费者。
|
270 | 272 |
|
| 273 | +## RocketMQ功能特性 |
| 274 | + |
| 275 | +### 消息 |
| 276 | + |
| 277 | +#### 普通消息 |
| 278 | + |
| 279 | +普通消息一般应用于微服务解耦、事件驱动、数据集成等场景,这些场景大多数要求数据传输通道具有可靠传输的能力,且对消息的处理时机、处理顺序没有特别要求。以在线的电商交易场景为例,上游订单系统将用户下单支付这一业务事件封装成独立的普通消息并发送至RocketMQ服务端,下游按需从服务端订阅消息并按照本地消费逻辑处理下游任务。每个消息之间都是相互独立的,且不需要产生关联。另外还有日志系统,以离线的日志收集场景为例,通过埋点组件收集前端应用的相关操作日志,并转发到 RocketMQ 。 |
| 280 | + |
| 281 | + |
| 282 | + |
| 283 | +**普通消息生命周期** |
| 284 | + |
| 285 | +- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。 |
| 286 | +- 待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。 |
| 287 | +- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ会对消息进行重试处理。 |
| 288 | +- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。RocketMQ默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。 |
| 289 | +- 消息删除:RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。 |
| 290 | + |
| 291 | +#### 定时消息 |
| 292 | + |
| 293 | +在分布式定时调度触发、任务超时处理等场景,需要实现精准、可靠的定时事件触发。使用RocketMQ 的定时消息可以简化定时调度任务的开发逻辑,实现高性能、可扩展、高可靠的定时触发能力。定时消息仅支持在 MessageType为Delay 的主题内使用,即定时消息只能发送至类型为定时消息的主题中,发送的消息的类型必须和主题的类型一致。 |
| 294 | + |
| 295 | +基于定时消息的超时任务处理具备如下优势: |
| 296 | + |
| 297 | +- **精度高、开发门槛低**:基于消息通知方式不存在定时阶梯间隔。可以轻松实现任意精度事件触发,无需业务去重。 |
| 298 | +- **高性能可扩展**:传统的数据库扫描方式较为复杂,需要频繁调用接口扫描,容易产生性能瓶颈。RocketMQ 的定时消息具有高并发和水平扩展的能力。 |
| 299 | + |
| 300 | + |
| 301 | + |
| 302 | +**定时消息生命周期** |
| 303 | + |
| 304 | +- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。 |
| 305 | +- 定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息**单独存储在定时存储系统中**,等待定时时刻到达。 |
| 306 | +- 待消费:定时时刻到达后,服务端将消息重新写入普通存储引擎,对下游消费者可见,等待消费者消费的状态。 |
| 307 | +- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,RocketMQ会对消息进行重试处理。 |
| 308 | +- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。 |
| 309 | +- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。 |
| 310 | + |
| 311 | +定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。 |
| 312 | + |
| 313 | +#### 顺序消息 |
| 314 | + |
| 315 | +顺序消息仅支持使用MessageType为FIFO的主题,即顺序消息只能发送至类型为顺序消息的主题中,发送的消息的类型必须和主题的类型一致。和普通消息发送相比,顺序消息发送必须要设置消息组。(推荐实现MessageQueueSelector的方式,见下文)。要保证消息的顺序性需要单一生产者串行发送。 |
| 316 | + |
| 317 | +单线程使用MessageListenerConcurrently可以顺序消费,多线程环境下使用MessageListenerOrderly才能顺序消费。 |
| 318 | + |
| 319 | +#### 事务消息 |
| 320 | + |
| 321 | +施工中。。。 |
| 322 | + |
| 323 | +## 关于发送消息 |
| 324 | + |
| 325 | +### **不建议单一进程创建大量生产者** |
| 326 | + |
| 327 | +Apache RocketMQ 的生产者和主题是多对多的关系,支持同一个生产者向多个主题发送消息。对于生产者的创建和初始化,建议遵循够用即可、最大化复用原则,如果有需要发送消息到多个主题的场景,无需为每个主题都创建一个生产者。 |
| 328 | + |
| 329 | +### **不建议频繁创建和销毁生产者** |
| 330 | + |
| 331 | +Apache RocketMQ 的生产者是可以重复利用的底层资源,类似数据库的连接池。因此不需要在每次发送消息时动态创建生产者,且在发送结束后销毁生产者。这样频繁的创建销毁会在服务端产生大量短连接请求,严重影响系统性能。 |
| 332 | + |
| 333 | +正确示例: |
| 334 | + |
| 335 | +```java |
| 336 | +Producer p = ProducerBuilder.build(); |
| 337 | +for (int i =0;i<n;i++){ |
| 338 | + Message m= MessageBuilder.build(); |
| 339 | + p.send(m); |
| 340 | + } |
| 341 | +p.shutdown(); |
| 342 | +``` |
| 343 | + |
| 344 | +## 消费者分类 |
| 345 | + |
| 346 | +### PushConsumer |
| 347 | + |
| 348 | +高度封装的消费者类型,消费消息仅仅通过消费监听器监听并返回结果。消息的获取、消费状态提交以及消费重试都通过 RocketMQ 的客户端SDK完成。 |
| 349 | + |
| 350 | +PushConsumer的消费监听器执行结果分为以下三种情况: |
| 351 | + |
| 352 | +- 返回消费成功:以Java SDK为例,返回`ConsumeResult.SUCCESS`,表示该消息处理成功,服务端按照消费结果更新消费进度。 |
| 353 | +- 返回消费失败:以Java SDK为例,返回`ConsumeResult.FAILURE`,表示该消息处理失败,需要根据消费重试逻辑判断是否进行重试消费。 |
| 354 | +- 出现非预期失败:例如抛异常等行为,该结果按照消费失败处理,需要根据消费重试逻辑判断是否进行重试消费。 |
| 355 | + |
| 356 | +具体实现可以参见这篇文章[RocketMQ对pull和push的实现 ](http://devedmc.com/archives/1691854198138)。 |
| 357 | + |
| 358 | +使用PushConsumer消费者消费时,不允许使用以下方式处理消息,否则 RocketMQ 无法保证消息的可靠性。 |
| 359 | + |
| 360 | +- 错误方式一:消息还未处理完成,就提前返回消费成功结果。此时如果消息消费失败,RocketMQ 服务端是无法感知的,因此不会进行消费重试。 |
| 361 | +- 错误方式二:在消费监听器内将消息再次分发到自定义的其他线程,消费监听器提前返回消费结果。此时如果消息消费失败,RocketMQ 服务端同样无法感知,因此也不会进行消费重试。 |
| 362 | +- PushConsumer严格限制了消息同步处理及每条消息的处理超时时间,适用于以下场景: |
| 363 | + - 消息处理时间可预估:如果不确定消息处理耗时,经常有预期之外的长时间耗时的消息,PushConsumer的可靠性保证会频繁触发消息重试机制造成大量重复消息。 |
| 364 | + - 无异步化、高级定制场景:PushConsumer限制了消费逻辑的线程模型,由客户端SDK内部按最大吞吐量触发消息处理。该模型开发逻辑简单,但是不允许使用异步化和自定义处理流程。 |
| 365 | + |
| 366 | +### SimpleConsumer |
| 367 | + |
| 368 | +SimpleConsumer 是一种接口原子型的消费者类型,消息的获取、消费状态提交以及消费重试都是通过消费者业务逻辑主动发起调用完成。 |
| 369 | + |
| 370 | +一个来自官网的例子: |
| 371 | + |
| 372 | +```java |
| 373 | +// 消费示例:使用 SimpleConsumer 消费普通消息,主动获取消息处理并提交。 |
| 374 | +ClientServiceProvider provider = ClientServiceProvider.loadService(); |
| 375 | +String topic = "YourTopic"; |
| 376 | +FilterExpression filterExpression = new FilterExpression("YourFilterTag", FilterExpressionType.TAG); |
| 377 | +SimpleConsumer simpleConsumer = provider.newSimpleConsumerBuilder() |
| 378 | + // 设置消费者分组。 |
| 379 | + .setConsumerGroup("YourConsumerGroup") |
| 380 | + // 设置接入点。 |
| 381 | + .setClientConfiguration(ClientConfiguration.newBuilder().setEndpoints("YourEndpoint").build()) |
| 382 | + // 设置预绑定的订阅关系。 |
| 383 | + .setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression)) |
| 384 | + // 设置从服务端接受消息的最大等待时间 |
| 385 | + .setAwaitDuration(Duration.ofSeconds(1)) |
| 386 | + .build(); |
| 387 | +try { |
| 388 | + // SimpleConsumer 需要主动获取消息,并处理。 |
| 389 | + List<MessageView> messageViewList = simpleConsumer.receive(10, Duration.ofSeconds(30)); |
| 390 | + messageViewList.forEach(messageView -> { |
| 391 | + System.out.println(messageView); |
| 392 | + // 消费处理完成后,需要主动调用 ACK 提交消费结果。 |
| 393 | + try { |
| 394 | + simpleConsumer.ack(messageView); |
| 395 | + } catch (ClientException e) { |
| 396 | + logger.error("Failed to ack message, messageId={}", messageView.getMessageId(), e); |
| 397 | + } |
| 398 | + }); |
| 399 | +} catch (ClientException e) { |
| 400 | + // 如果遇到系统流控等原因造成拉取失败,需要重新发起获取消息请求。 |
| 401 | + logger.error("Failed to receive message", e); |
| 402 | +} |
| 403 | +``` |
| 404 | + |
| 405 | +SimpleConsumer适用于以下场景: |
| 406 | + |
| 407 | +- 消息处理时长不可控:如果消息处理时长无法预估,经常有长时间耗时的消息处理情况。建议使用SimpleConsumer消费类型,可以在消费时自定义消息的预估处理时长,若实际业务中预估的消息处理时长不符合预期,也可以通过接口提前修改。 |
| 408 | +- 需要异步化、批量消费等高级定制场景:SimpleConsumer在SDK内部没有复杂的线程封装,完全由业务逻辑自由定制,可以实现异步分发、批量消费等高级定制场景。 |
| 409 | +- 需要自定义消费速率:SimpleConsumer是由业务逻辑主动调用接口获取消息,因此可以自由调整获取消息的频率,自定义控制消费速率。 |
| 410 | + |
| 411 | +### PullConsumer |
| 412 | + |
| 413 | +施工中。。。 |
| 414 | + |
| 415 | +## 消费者分组和生产者分组 |
| 416 | + |
| 417 | +### 生产者分组 |
| 418 | + |
| 419 | +RocketMQ 服务端5.x版本开始,**生产者是匿名的**,无需管理生产者分组(ProducerGroup);对于历史版本服务端3.x和4.x版本,已经使用的生产者分组可以废弃无需再设置,且不会对当前业务产生影响。 |
| 420 | + |
| 421 | +### 消费者分组 |
| 422 | + |
| 423 | +消费者分组是多个消费行为一致的消费者的负载均衡分组。消费者分组不是具体实体而是一个逻辑资源。通过消费者分组实现消费性能的水平扩展以及高可用容灾。 |
| 424 | + |
| 425 | +消费者分组中的订阅关系、投递顺序性、消费重试策略是一致的。 |
| 426 | + |
| 427 | +- 订阅关系:Apache RocketMQ 以消费者分组的粒度管理订阅关系,实现订阅关系的管理和追溯。 |
| 428 | +- 投递顺序性:Apache RocketMQ 的服务端将消息投递给消费者消费时,支持顺序投递和并发投递,投递方式在消费者分组中统一配置。 |
| 429 | +- 消费重试策略: 消费者消费消息失败时的重试策略,包括重试次数、死信队列设置等。 |
| 430 | + |
| 431 | +RocketMQ 服务端5.x版本:上述消费者的消费行为从关联的消费者分组中统一获取,因此,同一分组内所有消费者的消费行为必然是一致的,客户端无需关注。 |
| 432 | + |
| 433 | +RocketMQ 服务端3.x/4.x历史版本:上述消费逻辑由消费者客户端接口定义,因此,您需要自己在消费者客户端设置时保证同一分组下的消费者的消费行为一致。[来自官方网站] |
| 434 | + |
271 | 435 | ## 如何解决顺序消费和重复消费?
|
272 | 436 |
|
273 | 437 | 其实,这些东西都是我在介绍消息队列带来的一些副作用的时候提到的,也就是说,这些问题不仅仅挂钩于 `RocketMQ` ,而是应该每个消息中间件都需要去解决的。
|
|
0 commit comments