目录
前言
上一章我们了解了RocketMQ的一些基本概念,这章我们将会通过一些实例结合实际场景对一些消费模式进行了解,并通过源码解读的形式,深入了解其原理
本章节所有代码可以前往github下载自行查看——rocket demo
1、拉取式消费 & 推动式消费
假如你订阅了一个微信公众号,那么你查看公众号消息的形式无外乎两种:
1. 公众号给你推送消息
2. 你主动点开公众号去查看消息
对于RocketMQ的消费者而言,也同样有这两种模式。
1.1 拉取式消费(Pull)
在RocketMQ使用拉取式消费,其实就是用户主动去获取一次该主题下队列的所有消息,然后根据不同的情况自行处理。
拉取式消费的用法相对比较复杂,我们要先需要通过DefaultMQPullConsumer获取订阅主题下的队列,遍历队列后取出队列里的消息。
这里只放部分代码,具体demo可以去GitHub上面查看
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("TopicStudent");
for (MessageQueue mq : mqs) {
System.err.println("Consume from the queue: " + mq);
SINGLE_MQ:
while (true){
try {
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
System.out.println(pullResult);
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_NEW_MSG:
break SINGLE_MQ;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
1.2 推动式消费(Push)
上面的拉取式消费还只是部分代码就已经这么繁琐,相对于拉取式消费,推动式消费的代码量就简洁多了:
public class EasyConsumer {
private DefaultMQPushConsumer consumer;
public EasyConsumer() throws Exception {
consumer = new DefaultMQPushConsumer(JmsConfig.GROUP);
consumer.setNamesrvAddr(JmsConfig.NAME_SERVER);
consumer.subscribe(JmsConfig.TOPIC_EASY, "easy-message-1");
consumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
for (Message msg : list) {
//消费者获取消息 这里只输出 不做后面逻辑处理
System.out.println("---------消费消息:" + LocalTime.now());
String body = new String(msg.getBody(), StandardCharsets.UTF_8);
System.out.println(body);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.println("consumer启动...");
}
}
按照正常的逻辑去理解,推动式消费的原理应该是BrokerServer收到消息后主动通知消费者进行消费。然而我要告诉你的是,在RocketMQ中实际上是没有所谓的推动式消费。
所谓的推动式消费其实只是RocketMQ帮你封装了一个线程去轮询,不断的去做拉取式消费要做的事情。从使用者的层面来看,就好像是BrokerServer收到消息后主动通知你有新消息一样。
关于这一点我们在源码中也可以证实:
首先我们进入DefaultMQPushConsumer的start()方法,然后依次进入this.defaultMQPushConsumerImpl.start(),this.mQClientFactory.start();
然后我们观察这个类的start方法:
public void start() throws MQClientException {
synchronized(this) {
switch(this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
this.mQClientAPIImpl.start();
this.startScheduledTask();
this.pullMessageService.start();
this.rebalanceService.start();
this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
this.log.info("the client factory [{}] start OK", this.clientId);
this.serviceState = ServiceState.RUNNING;
case RUNNING:
case SHUTDOWN_ALREADY:
default:
return;
case START_FAILED:
throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", (Throwable)null);
}
}
}
事实上在这里我们就能看出端倪了,这个方法里面根本没有Push相关的代码,我们再点进去PullMessageService类就能发现,这个类实现了runnable接口,而它的start方法中做的事情,是不断从pullRequestQueue队列里获取消息。
public void run() {
this.log.info(this.getServiceName() + " service started");
while(!this.isStopped()) {
try {
PullRequest pullRequest = (PullRequest)this.pullRequestQueue.take();
this.pullMessage(pullRequest);
} catch (InterruptedException var2) {
} catch (Exception var3) {
this.log.error("Pull Message Service Run Method exception", var3);
}
}
this.log.info(this.getServiceName() + " service end");
}
整个流程大概如下图:
既然有从队列中取数据,那么就一定有从队列中放数据的方法,我们可以在这个类中找到put的相关代码:
public void executePullRequestLater(final PullRequest pullRequest, long timeDelay) {
if (!this.isStopped()) {
this.scheduledExecutorService.schedule(new Runnable() {
public void run() {
PullMessageService.this.executePullRequestImmediately(pullRequest);
}
}, timeDelay, TimeUnit.MILLISECONDS);
} else {
this.log.warn("PullMessageServiceScheduledThread has shutdown");
}
}
到了这里,相信各位读者已经能明白了,一开始我们进来的明明是Push的start()方法,可是最后干活的确是PullMessageService(这说明哪有什么岁月静好,只不过是PullMessageService帮你负重前行了)
2、广播消费 & 集群消费
顾名思义
1. 广播消费模式下,相同的Consumer Group的每个Consumer实例都接收全量的消息
2. 集群消费模式下,相同的Consumer Group的每个Consumer实例均摊消息
2.1 广播消费(BROADCASTING)
从代码而言,广播消费和集群消费的实现很简单,我们只需要设置consumer的message model即可:
consumer.setMessageModel(MessageModel.BROADCASTING);
2.2 集群消费(CLUSTERING)
consumer.setMessageModel(MessageModel.CLUSTERING);
3、顺序消费
假设我们在做一个电商系统,那么对于一个用户创建订单到完成支付更新整个流程而言,整个过程在rocketMQ应该保证其消费的有序性。
顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。
1. 全局顺序 对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景
2. 分区顺序 对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。 Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格的按照 FIFO 原则进行消息发布和消费的场景。
在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列),而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。
但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
因此从消费者的角度而言,我们不需要改动什么,只需要在生产者生产消息时保证投递到同一队列即可:
DefaultMQProducer mqProducer = orderedProduce.getProducer();
//创建生产信息
for (int i = 0; i < 5; i++) {
Message message = new Message(JmsConfig.TOPIC_ORDER, "order_message", ("创建订单:" + i).getBytes());
mqProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
int id = (int) o;
int index = id % list.size();
return list.get(index);
}
},i);
}
for (int i = 0; i < 5; i++) {
Message message = new Message(JmsConfig.TOPIC_ORDER, "order_message", ("支付订单:" + i).getBytes());
mqProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
int id = (int) o;
int index = id % list.size();
return list.get( index);
}
},i);
}
for (int i = 0; i < 5; i++) {
Message message = new Message(JmsConfig.TOPIC_ORDER, "order_message", ("发货:" + i).getBytes());
mqProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
int id = (int) o;
int index = id % list.size();
return list.get( index);
}
},i);
}
4 结尾
本章我们学习了一些简单方法的使用,但是对于电商而言,用户创建订单会有一个锁定库存超时自动释放的逻辑,针对这个场景,RocketMQ也有相应的延时消费模式可以解决,下一章,我们将学习如何使用延时消费解决锁库存问题及其实现原理