一、消费者
(一)消费者类型
消费者可以分为两种类型:
- 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一样需要设置各种参数,写处理消息的函数,同时还需要做额外的事情,主要有三个:
- 获取MessageQueue并遍历
因为一个Topic下面有多个Message Queue,如果需要获取所有消息,就需要通过遍历所有队列进行处理。 - 维护Offset
从一个Message Queue获取消息的时候,需要传入当前Message Queue的Offset参数(long类型的值),随着不断读取消息Offset会不断增长这个时候由用户负责把Offset存储下来,根据具体情况可以存到内存里、写到磁盘或者数据库里等 - 根据不同类型的消息状态进行不同的处理
主要有四种状态: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 ,使用的时候一般有下面几个步骤:
- 设置Producer的GroupName;
- 设置InstanceName,通过Producer的
setInstanceName
方法实现。当一个JVM启动多个producer的时候,需要通过设置InstanceName来进行区分,如果不设置的话,默认值为:“DEFAULT”; - 设置发送失败重试次数,通过Producer的
setRetryTimesWhenSendFailed
方法来实现。当网络出现异常的时候,这个次数影响消息的重复投递次数想保证不丢消息,可以设置多重试几次。 - 设置NameServer地址;
- 组装消息并发送。
(二)消息发送结果
消息发送的结果有如下四种(都定义在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:
- 定义自己的MessageQueueSelector,实现
MessageQueueSelector
接口 - 在发送消息的时候调用带有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。但是,并不能完全保证,比如,重试等,会打乱消息的顺序。