顺序消费
关于消息的消费过程中,可能遇到的异常情况,可以参考该博客:https://www.jianshu.com/p/453c6e7ff81c
②并且该队列只有一个消费者,也就是说 同一个队列,不能出现多个消费者并行消费的情况
到这里可能有的人会问,一个队列只有一个消费者,那性能岂不是很低?关于这个情况,rocketmq的解决方法是,虽然同一个队列不能并行消费,但是可以并行消费不同的队列。就是上面说的同时多笔订单之间又是可以并行消费。
下载github上的example例子https://github.com/apache/rocketmq
OrderProducer.java
将上面的OrderConsumer.java再次复制一份,日志添加 "消费者1" "消费者2" 用来区分 不同的消费者
启动C1,C2查看控制台消费者状况
消费者1日志如下:
消费者2日志如下:
什么是顺序消费?
指的是可以按照消息的发送顺序来消费。例如:一笔订单产生了 3 条消息,分别是订单创建、订单付款、订单完成。消费时,要按照顺序依次消费才有意义。与此同时多笔订单之间又是可以并行消费的。关于消息的消费过程中,可能遇到的异常情况,可以参考该博客:https://www.jianshu.com/p/453c6e7ff81c
rocketmq是怎么实现顺序消费的呢?
①rocketmq保证同一个订单的消息,一定要发送到同一个队列②并且该队列只有一个消费者,也就是说 同一个队列,不能出现多个消费者并行消费的情况
到这里可能有的人会问,一个队列只有一个消费者,那性能岂不是很低?关于这个情况,rocketmq的解决方法是,虽然同一个队列不能并行消费,但是可以并行消费不同的队列。就是上面说的同时多笔订单之间又是可以并行消费。
下载github上的example例子https://github.com/apache/rocketmq
顺序消费使用
查看下载的example的列子中,有个ordermessage可以拿来参考一下,然后自己写一个生产者消费者例子。OrderProducer.java
package com.test.order;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class OrderProducer {
public static void main(String[] args) throws MQClientException,
InterruptedException {
/**
* 一个应用创建一个Producer,由应用来维护此对象,可以设置为全局对象或者单例
* 注意:ProducerGroupName需要由应用来保证唯一
* ProducerGroup这个概念发送普通的消息时,作用不大,但是发送分布式事务消息时,比较关键
* 因为服务器会回查这个Group下的任意一个Producer
*/
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.setNamesrvAddr("172.20.1.186:9876");
producer.setInstanceName("Producer");
/**
* Rocket默认开启了VIP通道,VIP通道端口为10911-2=10909。若Rocket服务器未启动端口10909,则报connect to <> failed
*/
producer.setVipChannelEnabled(false);
/**
* Producer对象在使用之前必须要调用start初始化,初始化一次即可
* 注意:切记不可以在每次发送消息时,都调用start方法
*/
producer.start();
/**
* 下面这段代码表明一个Producer对象可以发送多个topic,多个tag的消息。
* 注意:send方法是同步调用,只要不抛异常就标识成功。但是发送成功也可会有多种状态,
* 例如消息写入Master成功,但是Slave不成功,这种情况消息属于成功,但是对于个别应用如果对消息可靠性要求极高
* 需要对这种情况做处理。另外,消息可能会存在发送失败的情况,失败重试由应用来处理
*/
for (int i = 1; i <= 10; i++) {
try {
/**
* TopicTest要发送的队列
* TagA标签,可以达到再次过滤,消费端可以只消费TagA的消息类似于这样
* key可以在控制台根据key查询消息
* body消息体内容
*/
Message msg = new Message("TopicTest",// topic
"TagA",// tag
"key1"+i,// key
("订单一号" + i).getBytes());// body
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//这里的arg就是orderId传进来的
Integer id=(Integer) arg;
//取模决定放在哪个数据库
int index = id % mqs.size();
return mqs.get(index);
}
},1);//订单号为1
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
TimeUnit.MILLISECONDS.sleep(1000);
}
for (int i = 1; i <= 10; i++) {
try {
int orderId = i;
Message msg = new Message("TopicTest",// topic
"TagA",// tag
"key2"+i,// key
("订单二号" + i).getBytes());// body
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//这里的arg就是orderId传进来的
Integer id=(Integer) arg;
//取模决定放在哪个数据库
int index = id % mqs.size();
return mqs.get(index);
}
},2);//订单号为2
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
TimeUnit.MILLISECONDS.sleep(1000);
}
for (int i = 1; i <= 10; i++) {
try {
int orderId = i;
Message msg = new Message("TopicTest",// topic
"TagA",// tag
"key3"+i,// key
("订单三号" + i).getBytes());// body
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//这里的arg就是orderId传进来的
Integer id=(Integer) arg;
//取模决定放在哪个数据库
int index = id % mqs.size();
return mqs.get(index);
}
},3);//订单号为3
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
TimeUnit.MILLISECONDS.sleep(1000);
}
/**
* 应用退出时,要调用shutdown来清理资源,关闭网络连接,从MetaQ服务器上注销自己
* 注意:我们建议应用在JBOSS、Tomcat等容器的退出钩子里调用shutdown方法
*/
producer.shutdown();
}
}
在获取到路由信息以后,会根据MessageQueueSelector实现的算法来选择一个队列,同一个OrderId获取到的肯定是同一个队列,这里的OrderIdId就是对应的不同的订单号
OrderConsumer.javapackage com.test.order;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import java.util.List;
public class OrderConsumer {
public static void main(String[] args) throws InterruptedException,
MQClientException {
/**
* 一个应用创建一个Consumer,由应用来维护此对象,可以设置为全局对象或者单例
* 注意:ConsumerGroupName需要由应用来保证唯一
*/
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("172.20.1.186:9876");
consumer.setInstanceName("Consumer1");
consumer.setMessageModel(MessageModel.CLUSTERING);
//消费线程最小数量
//consumer.setConsumeThreadMin(30);
//消费线程最大数量
//consumer.setConsumeThreadMax(100);
/**
* 订阅指定topic下tags分别等于TagA或TagC或TagD, 这里没有订阅TagB的消息,所以不会消费标签为TagB的消息,*代表不过滤 接受一切
*/
//consumer.subscribe("TopicTest", "TagA || TagC || TagD");
consumer.subscribe("TopicTest", "*");
/**
* 如果是顺序消息,这边的监听就要使用MessageListenerOrderly监听
* 并且,返回结果也要使用ConsumeOrderlyStatus
*/
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
//设置自动提交,如果不设置自动提交就算返回SUCCESS,消费者关闭重启 还是会重复消费的
context.setAutoCommit(true);
try {
for (MessageExt msg:msgs) {
System.out.println(" 消费者1 ==> 当前线程:"+Thread.currentThread().getName()+" ,quenuID: "+msg.getQueueId()+ " ,content: " + new String(msg.getBody()));
}
} catch (Exception e) {
e.printStackTrace();
//如果出现异常,消费失败,挂起消费队列一会会,稍后继续消费
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
//消费成功
return ConsumeOrderlyStatus.SUCCESS;
}
});
/**
* Consumer对象在使用之前必须要调用start初始化,初始化一次即可
*/
consumer.start();
System.out.println("C1 Started.");
}
}
注意:这里的Consumer使用的是MessageListenerOrderly类,不再使用MessageListenerConcurrently类。返回的状态也不再是ConsumeConcurrentlyStatus而是ConsumeOrderlyStatus
测试先启动Consumer端订阅,然后发布消息,查看打印日志
//消费线程最小数量,默认20
consumer.setConsumeThreadMin(30);
//消费线程最大数量,默认64
consumer.setConsumeThreadMax(100);
再次测试,负载均衡是否支持消息顺序执行
将上面的OrderConsumer.java再次复制一份,日志添加 "消费者1" "消费者2" 用来区分 不同的消费者
启动C1,C2查看控制台消费者状况
消费者1日志如下:
消费者2日志如下:
可以看到负载均衡,并且消息是有顺序的。同一个订单的消息都会放到同一个队列中。