文章目录
上篇文章《RocketMQ系列(二)双主双从集群搭建》中,我们搭建了双主双从的集群环境,并启动了可视化平台rocketmq-console,本文我们使用原生api发送一些消息到mq中,然后进行消费。
一.生产消息
导入mq客户端的依赖包:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
1.1 同步消息
public static void main(String[] args) throws Exception {
//1.创建消息生产者,并指定组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定nameserver的地址
producer.setNamesrvAddr("192.168.17.101:9876;192.168.17.102:9876");
//3.启动producer
producer.start();
//4.创建消息
for (int i = 0; i < 10; i++) {
Message message = new Message("base", "tag1", ("hello world" + i).getBytes());
//5.发送消息
SendResult sendResult = producer.send(message);
System.out.println(sendResult.toString());
TimeUnit.SECONDS.sleep(2);
}
//6.关闭生产者
producer.shutdown();
}
打印输出如下:
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C4F520000, offsetMsgId=C0A8116600002A9F00000000000000A9, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=2], queueOffset=1]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C57420001, offsetMsgId=C0A8116600002A9F0000000000000152, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=3], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C5F180002, offsetMsgId=C0A8116600002A9F00000000000001FB, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=0], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C66F90003, offsetMsgId=C0A8116600002A9F00000000000002A4, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=1], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C6ECE0004, offsetMsgId=C0A8116600002A9F000000000000034D, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=2], queueOffset=2]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C76A60005, offsetMsgId=C0A8116600002A9F00000000000003F6, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=3], queueOffset=1]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C7E7A0006, offsetMsgId=C0A8116600002A9F000000000000049F, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=0], queueOffset=1]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C86500007, offsetMsgId=C0A8116600002A9F0000000000000548, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=1], queueOffset=1]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C8E2C0008, offsetMsgId=C0A8116600002A9F00000000000005F1, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=2], queueOffset=3]
SendResult [sendStatus=SEND_OK, msgId=C0A8010603A018B4AAC2618C96030009, offsetMsgId=C0A8116600002A9F000000000000069A, messageQueue=MessageQueue [topic=base, brokerName=broker-b, queueId=3], queueOffset=2]
22:36:45.438 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.17.101:9876] result: true
22:36:45.443 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.17.101:10911] result: true
22:36:45.443 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.17.102:10909] result: true
22:36:45.443 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.17.102:11011] result: true
22:36:45.443 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.17.101:11011] result: true
22:36:45.443 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.17.102:10911] result: true
在mq-console控制台上观察如下:
topic:
运行两次后,产生20条信息:
1.2 异步消息
public static void main(String[] args) throws Exception {
//1.创建消息生产者,并指定组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定nameserver的地址
producer.setNamesrvAddr("192.168.17.101:9876;192.168.17.102:9876");
//3.启动producer
producer.start();
//4.创建消息
for (int i = 0; i < 10; i++) {
Message message = new Message("base", "tag2", ("hello world" + i).getBytes());
//5.发送消息
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送结果" + sendResult.toString());
}
@Override
public void onException(Throwable e) {
System.out.println("发送异常" + e);
}
});
TimeUnit.SECONDS.sleep(2);
}
//6.关闭生产者
producer.shutdown();
}
其中 SendCallback是回调函数,生产者的异步发送线程会根据发送消息的结果,调用成功或者失败的接口方法。
1.3 顺序消息
生产者把消息发送到broker时,由于每个broker内部不是只有1个队列,因此消费者也不是从某一个队列进行消费,因此默认生产者的消息顺序和消费者的消费顺序并不一定是一样的。
从rocketmq-console可以查看消费消息的队列情况:
上图信息可以看出,同一个broker-b,有0-3,共4个队列。
实际生产和消费的模型图如下:
因此,要向保证生产和消费的消息顺序一致,生产者和消费者必须使用同一个队列,生产消息的时候,必须把消息往同一个queue中推送。
但是这里要区分全局消息和局部消息,实际业务场景中,只要保证局部消息有序即可,比如张三和李四下单付款过程,只需要保证单个人下单、付款、推送的顺序即可,张三和李四俩人的消息顺序无所谓。
所以我们可以根据唯一标识,比如订单id,与队列数目取余,就是队列下标了,
demo如下:
//4.创建消息
//4.创建消息
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
Message message = new Message("orderTopic", "order", "i" + i, ("hello world" + i + "-" + j).getBytes());
//5.发送消息
int orderId = i;
SendResult sendResult = producer.send(message, new MessageQueueSelector() {
/**
*
* @param mqs broker的队列集合
* @param msg 消息对象
* @param arg 业务唯一标识,就是send方法的第3个参数
* @return
*/
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
int orderId = (int) arg;
return mqs.get(orderId % mqs.size());
}
}, orderId);
System.out.println("发送结果:" + sendResult);
TimeUnit.SECONDS.sleep(2);
}
}
上述demo,关键的地方是使用了带MessageQueueSelector参数的send方法。
我们使用变量 i 作为业务唯一标识orderId,总共发送了3组,每组4个消息。
1.4 延迟消息
发送延迟消息很简单,只需要对消息对象设置如下属性:
//设置消息延迟
message.setDelayTimeLevel(2);
其中,参数表示延迟时间的等级,具体等级如下,
//org\apache\rocketmq\store\config\MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
参数设置为2,表示延迟5s。
二.消息消费
2.1 广播模式
public static void main(String[] args) throws Exception {
//1.创建消费者,指定组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.指定nameserver地址
consumer.setNamesrvAddr("192.168.17.101:9876;192.168.17.102:9876");
//3.订阅主题和tag
consumer.subscribe("base", "tag1");
//广播模式||负载均衡模式
consumer.setMessageModel(MessageModel.BROADCASTING);
//4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.println(new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者
consumer.start();
}
说明:
- MessageModel.BROADCASTING:广播模式,当有多个消费者订阅同一个topic时,每个消费者消费的消息是相同的。
- consumer.subscribe(“base”, “tag1”); 可以同时订阅多个tag,比如tag1||tag2,也可以直接用 * 号代替,表示所有。
上面demo中,我们使用了DefaultMQPushConsumer,属于push模式,属于broker推送到消费者;
还有一种pull模式,DefaultMQPullConsumer,属于消费者主动去broker拉取消息。
2.2 负载均衡
只需要如下设置,或者不设置此属性,默认就是负载均衡模式,
consumer.setMessageModel(MessageModel.CLUSTERING);
MessageModel.CLUSTERING:属于集群模式,也叫负载均衡模式,当有多个消费者订阅同一个topic时,每个消费者轮流消费生产者的消息,每台消费者消费的并集和等于生产消息总和。
2.3 顺序消息
上面消费消息的时候,监听器的参数是MessageListenerConcurrently,实际还有一个重载方法,参数是MessageListenerOrderly,顺序消费,就是使用这个:
//顺序消费
consumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
for (MessageExt msg : msgs) {
System.out.println("线程名称" + Thread.currentThread().getName() +"消费消息" + new String(msg.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
原理:
保证一个队列的消息,由同一个线程进行消费。
上文中,顺序消息发送的时候,我们使用变量 i 作为业务唯一标识orderId,总共发送了3组,每组4个消息,存入broker时,应该使用了3个queue,
因此消费者顺序消费时,应该启用3个线程,分别针对broker的3个队列,运行结果如下:
线程名称ConsumeMessageThread_2消费消息hello world1-0
线程名称ConsumeMessageThread_1消费消息hello world2-0
线程名称ConsumeMessageThread_3消费消息hello world0-0
线程名称ConsumeMessageThread_2消费消息hello world1-1
线程名称ConsumeMessageThread_1消费消息hello world2-1
线程名称ConsumeMessageThread_3消费消息hello world0-1
线程名称ConsumeMessageThread_1消费消息hello world2-2
线程名称ConsumeMessageThread_2消费消息hello world1-2
线程名称ConsumeMessageThread_1消费消息hello world2-3
线程名称ConsumeMessageThread_2消费消息hello world1-3
线程名称ConsumeMessageThread_3消费消息hello world0-2
线程名称ConsumeMessageThread_3消费消息hello world0-3
从结果看出,实际确实使用了3个线程,每个线程消费的顺序是有序的,都是0123。
2.4 过滤消息
消费者在消费消息时,可以有针对性的进行消费,有两种方式。
1. tag过滤
在创建消费者的时候,我们需要指定订阅的组和tag,如下,
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
consumer.subscribe("myTopic", "tag1||tag2||tag3");
我们看下subcribe的方法描述:
/**
* Subscribe a topic to consuming subscription.
*
* @param topic topic to subscribe.
* @param subExpression subscription expression.it only support or operation such as "tag1 || tag2 || tag3" <br>
* if null or * expression,meaning subscribe all
* @throws MQClientException if there is any client error.
*/
@Override
public void subscribe(String topic, String subExpression) throws MQClientException {
this.defaultMQPushConsumerImpl.subscribe(topic, subExpression);
}
其中参数:
- topic: 订阅的主题,消费时只会找这个主题下的消息;
- subExpression : 发送消息时会指定tag参数,这里就是tag的名字,如果要从多个tag消费,可以写为tag1 || tag2 || tag3,或者直接写 “*”。
2. sql语法过滤
sql过滤,使用的subcribe方法如下:
/**
* Subscribe a topic by message selector.
*
* @param topic topic to consume.
* @param messageSelector {@link org.apache.rocketmq.client.consumer.MessageSelector}
* @see org.apache.rocketmq.client.consumer.MessageSelector#bySql
* @see org.apache.rocketmq.client.consumer.MessageSelector#byTag
*/
@Override
public void subscribe(final String topic, final MessageSelector messageSelector) throws MQClientException {
this.defaultMQPushConsumerImpl.subscribe(topic, messageSelector);
}
其中参数:
- topic: 订阅的主题,消费时只会找这个主题下的消息;
- messageSelector : 指定一段sql语句。
具体例子如下:
consumer.subscribe("filterSqlTopic", MessageSelector.bySql("i>5"));
其中sql就是 “i>5”,这个i是什么意思呢?它是生产者在生产消息时指定的属性,生产者例子如下:
for (int i = 0; i < 10; i++) {
Message message = new Message("filterSqlTopic", "tag1", ("hello world" + i).getBytes());
//设置消息属性
message.putUserProperty("i", String.valueOf(i));
//5.发送消息
SendResult sendResult = producer.send(message);
System.out.println(sendResult.toString());
TimeUnit.SECONDS.sleep(2);
}
上面生产者代码,发送了10条信息,putUserProperty方法输入了一个key和一个value,其中key的名字就是"i",值是i的值,也就是0到10;
那么消费者指定 MessageSelector.bySql(“i>5”),就是只会消费 i 值大于5的。
其中,sql的写法还支持类似如下几种方式:
- a>5 AND b=‘abc’ ; > ,>=,<,<=,BETWEEN
- IS NULL IS NOT NULL
以及其他sql中常用比较和逻辑符号。