RocketMQ——生产者和消费者
文章目录
- RocketMQ——生产者和消费者
- RocketMQ简介
- RocketMQ生产者
- RocketMQ消费者
- DefaultMQPushConsumer
- DefaultMQPullConsumer
RocketMQ简介
RocketMQ共有四个角色,分别是Producer、Consumer、Broker、NameServer,他们分别对应的作用如下:
- Producer:消息生产者,负责消息的生产发送
- Consumer:消息消费者,负责消息接收和使用
- Broker:负责消息的传输和暂存
- NameServer:负责协调整个消息队列,维护配置信息和状态信息
RocketMQ生产者
消息生产者向消息队列写入数据,在不同的业务场景中可能会有不同的需求,下面介绍消息生产者
public class producer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group2");//设置group名
producer.setNamesrvAddr("192.168.108.128:9876");//设置Nameserver地址
producer.setVipChannelEnabled(false);
producer.start();//开启生产者
Message msg = new Message("TopicTest2",
"TagA",
("MQTest").getBytes(RemotingHelper.DEFAULT_CHARSET));
msg.setDelayTimeLevel(3);//设置延迟发送消息
SendResult result = producer.send(msg);//发送消息
System.out.println(result);
producer.shutdown();
}
}
在这里使用了默认消息生产类DefaultMQProducer,并且在创建好消息后通过调用setDelayTimeLevel(int level)方法设置延迟时间。
RocketMQ消费者
消费者可以分为两类
- DefaultMQPushConsumer:系统控制读取操作
- DefaultMQPullConsumer:使用者自主控制读取操作
但是这两个的本质都是通过Consumer轮询Broker然后再拉取消息,即pull模式
DefaultMQPushConsumer
DefaultMQPushConsumer通过用户设置好的参数等,系统会自动根据用户设置参数进行消息处理,自动保存Offset、做负载均衡等。
public class consumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group2");//设置group名
consumer.setNamesrvAddr("192.168.108.128:9876");//设置Nameserver地址
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);//设置读取位置,从最小的offset开始
consumer.subscribe("TopicTest2", "*");//生命接受的Topic名以及过滤tags
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
System.out.println(Thread.currentThread().getName() + " Receive Message: " + list);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
System.out.println("consumer start...");
consumer.start();
}
}
在程序中创建好了consumer对象之后,需要对Group、MessageModel、NameServer、Topic进行设置:
- Group主要将多个Consumer组织到一起,提高并发能力
- RocketMQ支持两种MessageModel消息模式:Clustering和BroadCasting。Clustering模式下,在同一个group中的消费者只会消费消息的一部分,即group内所有消费者的消费消息的总和是Topic内容整体。Broadcasting即广播模式,消息会同时发布到group内的每一个consumer。
- NameServer需要配置ip和端口号,多个NameServer用分号(;)隔开
- Topic是消息队列的标识,表明消费者需要消费哪些消息,并且可以通过消息的tags来进行过滤,如:consumer.subscribe(“TopicTest2”, “tag1||tag2||tag3”);此处使用 * 表示消费topic中的所有消息
在前面说过,DefaultMQPushConsumer虽然叫Push,其原理依然是Pull模式,怎么实现用Pull达到Push效果的呢?
首先我们来看看Push和Pull两种方式的优缺点:
- Push方式是在服务端接收到消息后立刻将消息推送到客户端去,这样的方式实时性高,但是如果在消息队列中使用这种方式首先是加大服务端的压力,一收到消息就立刻推送到客户端,会降低服务端的部分性能。其次,如果在推送过程中如果客户端发生阻塞,导致客户端不能及时处理消息,消息会在客户端堆积,从而造成各种潜在问题。
- Pull方式是客户端向服务端发起请求拉取数据,这种方式的优点是由客户端控制消息接收,可以使客户端处理完消息后再去服务端拉取下一条消息,防止消息在客户端堆积。这样的方式的缺点是需要对客户端拉取消息时间间隔进行把握,间隔过长实时性较低,间隔过短同样会造成消息堆积。
DefaultMQPushConsumer的主要功能都在类DefaultMQPushConsumerImpl中,这个类实现MQConsumerInner接口,该类中有个很长的方法叫public void pullMessage(final PullRequest pullRequest) ,该方法是处理消息的主要方法,并且在DefaultMQPushConsumer中又有非常多的Pull方法。
那么RocketMQ是怎样做到用Pull的方式实现Push的效果呢?其实PushConsumer是通过长轮询的方式来达到Push的效果。Broker将Consumer发送过来的拉取请求保持住,然后Consumer循环查看队列中是否有消息,如果没有消息则等待5秒,然后再去查看队列是否有消息,如果有消息则拉取,如果没有消息,就继续等待。当超过设置的BrokerSuspendMaxTimeMillis时(默认时15s,在默认情况下Consumer会等待3次),返回空结果。此时消息的控制权在Consumer上,大大减少了Consumer客户端的消息处理压力,但是在保持链接时会暂用一定的资源。
DefaultMQPullConsumer
下面代码是org.apache.rocketmq.example.simple中的示例代码,该代码演示了从消息队列中使用Pull模式拉取消息的步骤
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("please_rename_unique_group_name_5");
consumer.start();
//根据Topic名获取队列
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest1");
for (MessageQueue mq : mqs) {
//获取消息的offset
long Offset = Consumer.fetchConsumeOffset(mq, true);
System.out.printf("Consume from the queue: %s%n", mq);
SINGLE_MQ:
while (true) {
try {
PullResult pullResult =
consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
System.out.printf("%s%n", pullResult);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
//根据不同的效应进行处理
switch (pullResult.getPullStatus()) {
case FOUND:
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 long getMessageQueueOffset(MessageQueue mq) {
Long offset = OFFSE_TABLE.get(mq);
if (offset != null)
return offset;
return 0;
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
OFFSE_TABLE.put(mq, offset);
}
}
使用Pull模式拉取消息额外做了三件事
- 获取MessageQueue并遍历。因为一个Topic可能包含多个MessageQueue,因此需要对MessageQueue进行遍历才能获取;
- 维护Offsetstore。从MessageQueue拉去消息时,需要传入Offset值,随着MessageQueue的增长,需要用户存储Offset;
- 根据不同响应状态进行处理。拉去请求会返回FOUND(获取消息)、NO_MATCHED_MSG(没有合适消息)、NO_NEW_MSG(没有新消息)、OFFSET_ILLEGAL(不合法的Offset)