一、消费者

(一)消费者类型

消费者可以分为两种类型:

  • DefaultMQPushConsumer:由操作系统控制读取操作,收到消息后自动调用传入的处理方法来处理;
  • DefaultMQPullConsumer:读取操作中的大部分功能需要用户自主控制。

(二)DefaultMQPushConsumer

使用解释

使用DefaultMQPushConsumer主要设置好各种参数和传入处理消息的函数即可,主要参数有三个:

  • 当前Consumer所在GroupName。我们在上面构造的时候传入的,用于把多个Consumer组织在一起,提高并发能力,在不同的消息模型下,会产生不同的结果。
    Clustering模式下:每个消费者只是消费了所有消息中的一部分,所有的消费者消费的内容合起来才是整个topic内容的整体。
    BroadCasting模式下:同一个消费者组中的所有消费者都会接受所有的消息。
  • NameServer的地址和端口号。如果有多个NameServer,那么需要使用;连接,如:ip1:port1;ip2:port2;ip3:port3
  • Topic名称和tag。可以看到,我们通过consumer.subscribe("mytopic", "*")方法来订阅消息,指定了topic和tag,其中主题一般情况下是一种业务场景使用一个,而这个主题下的消息,我们也只进行部分消费,也就是使用tag来标识,“*”或者是null 标识消费所有,"tag1 || tag2 || tag3"标识带有这三个tag的消息会被消费,也可以使用通配符如tag*标识所有以tag打头的
消息处理流程

真正用来处理消息接收的,是封装在DefaultMQPushConsumer里面的DefaultMQPushConsumerImpl,消息的处理逻辑主要在pullMessage方法的PullCallback中。

PullCallback pullCallback = new PullCallback() {
  public void onSuccess(PullResult pullResult) {
    if (pullResult != null) {
      pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult, subscriptionData);
      switch(pullResult.getPullStatus()) {
      case FOUND:
        ......
      case NO_NEW_MSG:
        ......
      case NO_MATCHED_MSG:
        ......
      case OFFSET_ILLEGAL:
        ......
    }
  }
}

可以看到,会根据broker中返回的消息状况,进行不同的处理。
而且,我们可以看到很多PullRequest语句,如:DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); 也就是说,在DefaultMQPushConsumer中处理消息,也是通过pull方式的,只不过,采用的是“长轮询”的方式,从而达到push的效果。

长轮询
  • Push方式
    Push方式Server端接收到消息后,主动把消息推送给Client端,实时性高对于个提供队列服务的Server来说,用Push方式主动推送有很多弊端:首先是加大Server端的作量,进而影响Server的性能;其次,Client处理能力各不相同,Client的状态不受Server控制,如果Client不能及时处理Server推送过来的消息,会造成各种潜在问题。
  • Pull方式
    Pull方式是Client端循环地从Server端拉取消息,主动权在Client手里,自己拉取到消息后,处理妥当了再接Pull方式的问题是循环拉取消息的间隔不好设定,间隔太短就处在一个忙等”的状态,浪费资源;每个Pull的时间间隔太长Server端有消息到来时有可能没有被及时处理。
  • 长轮询方式
    通过Client端和Server端的配合,达到既拥有Pull的优点,又能达到保证实时性的目的。
    服务端接到新消息请求后,如果队列里没有新消息,并不急于返回,通过个循环不断查看状态,每次waitForRunning一段时间(默认是5秒)然后再Check。默认情况下当Broker一直没有新消息,第三次heck的时候,等待时间超过Request里面的BrokerSuspenMaxTimeMillis就返回空结果。在等待的过程中,Broker收到了新的消息后会直接调用notifyMessageArriving函数返回请求结果。“长轮询”的核是,Broker端HOLD住客户端过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给Consumer“长轮询”的主动权。还是握在Consumer手中,Broker即使有大消息积,也不会主动推送给Consumer
    长轮询方式的局限性,是在HOLD住Consumer求的时候需要占用资源,它适合用在消息队列这种客户端连接数可控的场景中。
流量控制

Consumer采用的是线程池的方式来处理pull下来的消息,然而,如果把Pull下来的消息直接提交给线程池,那么就很难监控和控制,比如:如何得知当前消息堆积的数量?如和重复处理某些消息?如何延迟处理某些消息?
RocketMQ定义了一个快照类ProcessQueue来解决这个问题,在Push Consumer运行的时候,每个MessageQueue有个对应的ProcesQueue 对象,保存了这个MessageQueue消息处理状态的快照。
ProcessQueue对象里主要的内容是一个TreeMap和一个读写锁。TreeMap里以MessageQueue的Offset作为Key,以消息内容的引用为Value,保存了所有从MessageQueue获取到,但是还未被处理的消息;读写锁控制着多个线程对TreeMap对象的并发访访问。
接下来,PushConsumer会判断获取到但还未处理的消息个数、消息总大小、Offset跨度,任何一个值超过设定的大小就隔一段时间再拉取消息,从而达到流量控制的目的此外ProcessQueue还可以辅助实现顺序消费的逻辑。

(三)DefaultMQPullConsumer

使用DefaultMQPulConsumer像使用DefaultMQPushConsumer一样需要设置各种参数,写处理消息的函数,同时还需要做额外的事情,主要有三个:

  1. 获取MessageQueue并遍历
    因为一个Topic下面有多个Message Queue,如果需要获取所有消息,就需要通过遍历所有队列进行处理。
  2. 维护Offset
    从一个Message Queue获取消息的时候,需要传入当前Message Queue的Offset参数(long类型的值),随着不断读取消息Offset会不断增长这个时候由用户负责把Offset存储下来,根据具体情况可以存到内存里、写到磁盘或者数据库里等
  3. 根据不同类型的消息状态进行不同的处理
    主要有四种状态:FOUND、NO_MATCHED_MSG、NO_NEW_MSG、OFFSET_ILLEGAL。其中比较重要的是FOUND(有新消息)和NO_NEW_MSG(没有消息)
使用示例
package com.firewolf.rocketmq;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;

/**
 * 作者:刘兴 时间:2019-07-15
 **/
public class PullConsumer {

  private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();

  public static void main(String[] args) throws MQClientException {
    DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("consumer-group1");

    consumer.setNamesrvAddr("10.211.55.6:9876");
    consumer.start();

    Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("mytopic");
    for (MessageQueue mq : mqs) {
      System.out.printf("Consume from the queue: %s%n", mq);
      SINGLE_MQ:
      while (true) { //先获取完一个Message Queue中的消息
        try {
          PullResult pullResult =
              consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
          System.out.printf("%s%n", pullResult);
          //更新offset
          putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
          switch (pullResult.getPullStatus()) {
            case FOUND:
              dealPullResult(pullResult);
              break;
            case NO_MATCHED_MSG:
              break;
            case NO_NEW_MSG:
              break SINGLE_MQ;
            case OFFSET_ILLEGAL:
              break;
            default:
              break;
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }

    consumer.shutdown();
  }

  private static void dealPullResult(PullResult pullResult) {

    List<MessageExt> msgFoundList = pullResult.getMsgFoundList();
    for (MessageExt messageExt : msgFoundList) {
      System.out.println(new String(messageExt.getBody()));
    }

  }

  //获取offset
  private static long getMessageQueueOffset(MessageQueue mq) {
    Long offset = OFFSE_TABLE.get(mq);
    if (offset != null) {
      return offset;
    }
    return 0;
  }


  //更新offset
  private static void putMessageQueueOffset(MessageQueue mq, long offset) {
    OFFSE_TABLE.put(mq, offset);
  }
}

(四)启动、关闭

消息队列一般是提供一个不间断的持续性服务,Consumer在使用过程中,如何才能优雅地启动和关闭,确保不漏掉或者重复消费消息呢?
Consumer分为PushPull两种方式,对于PullConsumer来说,使用者主动权很高,可以根据实际需要暂停、停止、启动消费过程需要注意的是Offset的保存,要在程序的异常处理部分增加把Offset写人磁盘方面的处理。
DefaultMQPushConsumer的退出,要调用hutdown()函数,以便释放资源、保存Offsfset等。这个调用要加到Consumer所在应用的退出逻辑中。

二、生产者

在不同的场景,会有不同类型的生产者。比如:同步发送、异步发送、单向发送、延迟发送、发送事务消息等。
在编程入门的地方,分别使用了同步发送、异步发送、单向发送。接下来,继续对一些知识进行补充。

(一)使用生产者主要步骤

生产者通常使用的是DefaultMQProducer ,使用的时候一般有下面几个步骤:

  1. 设置Producer的GroupName;
  2. 设置InstanceName,通过Producer的setInstanceName方法实现。当一个JVM启动多个producer的时候,需要通过设置InstanceName来进行区分,如果不设置的话,默认值为:“DEFAULT”;
  3. 设置发送失败重试次数,通过Producer的setRetryTimesWhenSendFailed方法来实现。当网络出现异常的时候,这个次数影响消息的重复投递次数想保证不丢消息,可以设置多重试几次。
  4. 设置NameServer地址;
  5. 组装消息并发送。

(二)消息发送结果

消息发送的结果有如下四种(都定义在SendStatus枚举中):

  • FLUSH_DISK_TIMEOUT:表示没有在规定时间内完成刷盘(需要Broker的刷盘策略被设置成SYNC_FLUSH才会报这个错误)
  • FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且Broker被设SYNC_MASTER方式,没有在设定时间内完成从同步。
  • SLAVE_NOT_AVAILABLE:这个状态产生的场景和FLUSH_SLAVE_TIMEOUT类似,表示在主备方式下,并且Broker被设置成SYNC_MASTER,但是没有找到被配置成SlaveBroker。
  • SEND_OK:表示发送成功,发送成功的具体含义,比如消息是否已经被存储到融盘?消息是否被同步到了slave上?消息在ave上是否被写人磁盘?需要结合所配置的刷盘策略、主从策略来定这个状态还可以简单理解为,没有发生上面列出的个问题状态就是SEND_OK。

要写一个高质量的生产者,需要针对不同的发送结果做完善的考虑

(三)发送延时消息

可以通过Message的setDelayTimeLevel方法来让消息延时发送,默认级别有:“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”,当我们把级别设置成3的时候,就会延时10秒。

(四)自定义消息发送规则

为了提高效率,一个Producer通常会对应多个Message Queue,如果使用默认配置,这个Producer会轮流往各个Message Queue发送消息。Consumer在消费消息的时候,会根据负载均衡略,消费被分配到的Message Queue,如果不经过特定的设置,某条消息被发往哪个MessageQueue,被哪个Consumer消费是未知的。
我们可以通过如下方式来根据自己的需要决定哪个Message进入特定的MessageQueue:

  1. 定义自己的MessageQueueSelector,实现MessageQueueSelector接口
  2. 在发送消息的时候调用带有MessageQueueSelector参数的方法,如:public SendResult send(Message msg, MessageQueueSelector selector, Object arg)

如:

SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
        public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
          return mqs.get(0);
        }
      }, null);

如果我们需要接受全局有序的消息,就可以采用这种方式,让所有的消息进入同一个Message Queue。但是,并不能完全保证,比如,重试等,会打乱消息的顺序。