
1.概述
原创在:https://blog.csdn.net/zhangcongyi420/article/details/90548393
在RocketMQ中一般有两种获取消息的方式,一个是拉(pull,消费者主动去broker拉取),一个是推(push,主动推送给消费者)
区别是:
push方式里,consumer把轮询过程封装了,并注册MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。
pull方式里,取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。
从下面这张简单的示意图也可以大致看出其中的差别,相当于是说,push的方式是:消息发送到broker后,如果是push,则broker会主动把消息推送给consumer即topic中,而pull的方式是:消息投递到broker后,消费端需要主动去broker上拉消息,即需要手动写代码实现,
两种方式的优缺点对比:
push
实时性高,但增加服务端负载,消费端能力不同,如果push的速度过快,消费端会出现很多问题
pull
消费者从server端拉消息,主动权在消费端,可控性好,但是时间间隔不好设置,间隔太短,则空请求会多,浪费资源,间隔太长,则消息不能及时处理
在前面的几篇中均使用的是pushConsumer的方式实现的,在rocketmq中,我们知道对于每个指定的topic,默认的队列数量是4个,对于producer来说,发送消息到topic的时候,会随机为消息选择一个投递的队列,队列序号是 0~3
但是在实际业务中,有一些比较特殊的需要,比如顺序消费(本篇暂时不讲),其基本的原理就是通过指定队列来实现,更为常见的是,如果再某些情况下,我们如果不对消息指定发送顺序的话,消息会随机投递到队列,那么对于消费端来说,不好做负载均衡的消息分配;
设想,假如我们在某次电商抢购中需要生产两种消息,一个是产生订单的消息,另一个是发送短信的消息,而且我们的服务器数量有限,那么节省资源的方式就是通过指定队列也就是queueId来实现分类消息的发送,对于消费端来说,只需要通过上述的pullConsumer的方式从相同的topic下面获取指定的queueId的消息即可,
接下来我们使用代码来实现一下,
public class ProducerQueueSelector {
public static void main(String[] args) throws Exception {
// 声明并初始化一个producer
DefaultMQProducer producer = new DefaultMQProducer("producer1");
// 设置NameServer地址,此处应改为实际NameServer地址,多个地址之间用;分隔
// NameServer的地址必须有,但是也可以通过环境变量的方式设置,不一定非得写死在代码里
producer.setNamesrvAddr("192.168.111.132:9876");
producer.setVipChannelEnabled(false);//3.2.6的版本没有该设置,在更新或者最新的版本中务必将其设置为false,否则会有问题
producer.setRetryTimesWhenSendFailed(3);
// 调用start()方法启动一个producer实例
producer.start();
// 发送10条消息到Topic为TopicTest,tag为TagA,消息内容为“Hello RocketMQ”拼接上i的值
for (int i = 0; i < 10; i++) {
try {
Message msg = new Message("TopicTest", // topic
"TagA", // tag
"i" + i, ("Hello RocketMQ " + i).getBytes("utf-8")// body
);
// 调用producer的send()方法发送消息
// 这里调用的是同步的方式,所以会有返回结果
SendResult sendResult = producer.send(msg);
//指定消息投递的队列,同步的方式,会有返回结果
/*SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> queues, Message msg, Object queNum) {
int queueNum = Integer.parseInt(queNum.toString());
return queues.get(queueNum);
}
}, 0);*/
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "," + i);
// System.out.println(sendResult.getSendStatus()); //发送结果状态
// 打印返回结果,可以看到消息发送的状态以及一些相关信息
System.out.println("当前消息投递到的队列是 : " + sendResult.getMessageQueue().getQueueId());
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 发送完消息之后,调用shutdown()方法关闭producer
producer.shutdown();
}
}
第一种方式,直接发送,启动程序,我们观察一下控制台的打印结果,
可以看到,消息是随机发送到当前topic下面的不同的队列中的,我们再看消费端代码,
public class QueueConsumer {
private static final Map<MessageQueue, Long> offsetTable = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws Exception {
offsetTable.clear();
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pullConsumer");
consumer.setNamesrvAddr("192.168.111.132:9876");
consumer.start();
try {
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest");
for (MessageQueue mq : mqs) {
// System.out.println("Consume from the queue: " + mq);
System.out.println("当前获取的消息的归属队列是: " + mq.getQueueId());
// if (mq.getQueueId() == 0) {
//System.out.println("我是从第1个队列获取消息的");
// long offset = consumer.fetchConsumeOffset(mq, true);
// PullResultExt pullResult
// =(PullResultExt)consumer.pull(mq,
// null, getMessageQueueOffset(mq), 32);
// 消息未到达默认是阻塞10秒,private long consumerPullTimeoutMillis =
// 1000 *
// 10;
PullResultExt pullResult = (PullResultExt) consumer.pullBlockIfNotFound(mq, null,
getMessageQueueOffset(mq), 32);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
System.out.println("收到了消息:" + new String(m.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break;
case OFFSET_ILLEGAL:
break;
default:
break;
}
}
// }
} catch (MQClientException e) {
e.printStackTrace();
}
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offsetTable.put(mq, offset);
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = offsetTable.get(mq);
if (offset != null)
return offset;
return 0;
}
}
运行程序,我们观察一下控制台打印结果,可以看到,通过pullConsumer拉取消息需要从所有的messageQueue中获取消息遍历然后取出所有的消息进行消费,不同的queueId中的消息可能不同,
这时,我们在生产者代码里面,指定队列发送消息,需要使用到messageQueueSelector这个回调函数,具体用法比较简单,大家可以在API中看到,修改后的代码如下,
public class ProducerQueueSelector {
public static void main(String[] args) throws Exception {
// 声明并初始化一个producer
DefaultMQProducer producer = new DefaultMQProducer("producer1");
// 设置NameServer地址,此处应改为实际NameServer地址,多个地址之间用;分隔
// NameServer的地址必须有,但是也可以通过环境变量的方式设置,不一定非得写死在代码里
producer.setNamesrvAddr("192.168.111.132:9876");
producer.setVipChannelEnabled(false);//3.2.6的版本没有该设置,在更新或者最新的版本中务必将其设置为false,否则会有问题
producer.setRetryTimesWhenSendFailed(3);
// 调用start()方法启动一个producer实例
producer.start();
// 发送10条消息到Topic为TopicTest,tag为TagA,消息内容为“Hello RocketMQ”拼接上i的值
for (int i = 0; i < 10; i++) {
try {
Message msg = new Message("TopicTest", // topic
"TagA", // tag
"i" + i, ("你好,rocketMq " + i).getBytes("utf-8")// body
);
// 调用producer的send()方法发送消息
// 这里调用的是同步的方式,所以会有返回结果
//SendResult sendResult = producer.send(msg);
//指定消息投递的队列,同步的方式,会有返回结果
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> queues, Message msg, Object queNum) {
int queueNum = Integer.parseInt(queNum.toString());
return queues.get(queueNum);
}
}, 0);
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "," + i);
// System.out.println(sendResult.getSendStatus()); //发送结果状态
// 打印返回结果,可以看到消息发送的状态以及一些相关信息
System.out.println("当前消息投递到的队列是 : " + sendResult.getMessageQueue().getQueueId());
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
// 发送完消息之后,调用shutdown()方法关闭producer
producer.shutdown();
}
}
启动程序,我们看到,所有的消息都被发送到queueId为0的队列中了,
consumer端为了能够看出效果,我们在代码中也稍作调整,遍历messageQueue的时候,筛选queueId为0的消息,consumer端代码我们调整如下,
public class QueueConsumer {
private static final Map<MessageQueue, Long> offsetTable = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws Exception {
offsetTable.clear();
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pullConsumer");
consumer.setNamesrvAddr("192.168.111.132:9876");
consumer.start();
try {
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicTest");
for (MessageQueue mq : mqs) {
//System.out.println("Consume from the queue: " + mq);
//System.out.println("当前获取的消息的归属队列是: " + mq.getQueueId());
if (mq.getQueueId() == 0) {
// long offset = consumer.fetchConsumeOffset(mq, true);
// PullResultExt pullResult
// =(PullResultExt)consumer.pull(mq,
// null, getMessageQueueOffset(mq), 32);
// 消息未到达默认是阻塞10秒,private long consumerPullTimeoutMillis =
// 1000 *
// 10;
PullResultExt pullResult = (PullResultExt) consumer.pullBlockIfNotFound(mq, null,
getMessageQueueOffset(mq), 32);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
switch (pullResult.getPullStatus()) {
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
System.out.println("我是从第1个队列获取消息的");
System.out.println("收到了消息:" + new String(m.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break;
case OFFSET_ILLEGAL:
break;
default:
break;
}
}
}
} catch (MQClientException e) {
e.printStackTrace();
}
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
offsetTable.put(mq, offset);
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = offsetTable.get(mq);
if (offset != null)
return offset;
return 0;
}
}
运行这段程序,我们看到控制台打印结果,可以看到,经过筛选之后,我们只从queueId=0的队列中获取消息,
需要注意的是,如果使用pullConsumer,在消费端程序中需要设定offSet,即偏移量的设置,关于偏移量的问题,大家可以先自行查找一下相关的资料,会在以后继续探讨,本篇的讲解到此结束,最后感谢观看!