4、消息类型
4.1、普通消息
对于普通消息,RocketMQ提供了三种发送⽅式:可靠同步发送、可靠异步发送和单向发送
1、可靠同步发送
同步发送是指消息发送⽅发出数据后,会在收到接收⽅发回响应之后才会发送下⼀个数据包的通讯⽅式。
此种⽅式应⽤场景⾮常⼴泛,例如重要通知邮件、报名短信通知、营销短信系统等。
package org.apache.rocketmq.example.common.product; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingException; import java.io.UnsupportedEncodingException; public class SyncMessage { public static void main(String[] args) throws UnsupportedEncodingException, MQClientException, MQBrokerException, RemotingException, InterruptedException { DefaultMQProducer producer = new DefaultMQProducer("my_test_sync_group"); producer.setNamesrvAddr("localhost:9876"); producer.start(); for (int i = 0; i < 100; i++) { Message msg = new Message("TopicTest" , "TagA", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) ); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } producer.shutdown(); } }
2、可靠异步发送
异步发送是指发送⽅发出数据后,不等接收⽅发回响应,接着发送下⼀个数据包的通讯⽅式。发送⽅通过回调接⼝接收服务器响应,并对响应结果进⾏处理。
package org.apache.rocketmq.example.common.product; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.common.RemotingHelper; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class AsyncMessage { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("my_test_aysnc_group"); producer.setNamesrvAddr("localhost:9876"); producer.start(); producer.setRetryTimesWhenSendAsyncFailed(0); int messageCount = 100; final CountDownLatch countDownLatch = new CountDownLatch(messageCount); for (int i = 0; i < messageCount; i++) { try { final int index = i; Message msg = new Message("Jodie_topic_1023", "TagA", "OrderID188", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); producer.send(msg, new SendCallback() { public void onSuccess(SendResult sendResult) { countDownLatch.countDown(); System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); } public void onException(Throwable e) { countDownLatch.countDown(); System.out.printf("%-10d Exception %s %n", index, e); e.printStackTrace(); } }); } catch (Exception e) { e.printStackTrace(); } } countDownLatch.await(5, TimeUnit.SECONDS); producer.shutdown(); } }
3、单向发送
单向发送是指发送⽅只负责发送消息,不等待服务器回应,且没有回调函数触发。即只发送请求⽽不管响应。
适⽤于某些耗时⾮常短,但对可靠性要求并不⾼的场景,例如⽇志收集。
package org.apache.rocketmq.example.common.product; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.common.RemotingHelper; public class OnewayProducer { public static void main(String[] args) throws Exception{ DefaultMQProducer producer = new DefaultMQProducer("my_test_oneway_group"); producer.setNamesrvAddr("192.168.8.147:9876"); producer.start(); for (int i = 0; i < 100; i++) { Message msg = new Message("TopicTest", "TagA", "666", ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) ); producer.sendOneway(msg); } Thread.sleep(5000); producer.shutdown(); } }
4、三种发送⽅式对⽐
发送⽅式 | 发送TPS | 发送结果反馈 | 可靠性 |
---|---|---|---|
同步发送 | 快 | 有 | 不丢失 |
异步发送 | 快 | 有 | 不丢失 |
单向发送 | 最快 | ⽆ | 可能丢失 |
4.2、顺序消息
消息有序指的是,消费者端消费消息时,需按照消息的发送顺序来消费,即先发送的消息,需要先消费(FIFO)。
RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。
全局有序:全局顺序时使⽤⼀个queue;
分区有序:局部顺序时多个queue并⾏消费;
1、全局顺序消息
需要将所有消息都发送到同⼀个队列,然后消费者端也订阅同⼀个队列,这样就能实现顺序消费消息的功能。下⾯通过⼀个示例说明如何实现全局顺序消息。
⽣产者:
package org.apache.rocketmq.example.common.orderMessage; import org.apache.rocketmq.client.exception.MQBrokerException; 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 org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingException; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.concurrent.ExecutionException; public class OrderMQProducer { public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException, ExecutionException, MQBrokerException { // 创建DefaultMQProducer类并设定生产者名称 DefaultMQProducer mqProducer = new DefaultMQProducer("producer-group-test"); // 设置NameServer地址,如果是集群的话,使用分号;分隔开 mqProducer.setNamesrvAddr("127.0.0.1:9876"); // 启动消息生产者 mqProducer.start(); for (int i = 0; i < 5; i++) { // 创建消息,并指定Topic(主题),Tag(标签)和消息内容 Message message = new Message("GLOBAL_ORDER_TOPIC", "", ("全局有序消息" + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); // 实现MessageQueueSelector,重写select方法,保证消息都进入同一个队列 // send方法的第一个参数: 需要发送的消息Message // send方法的第二个参数: 消息队列选择器MessageQueueSelector // send方法的第三个参数: 消息将要进入的队列下标,这里我们指定消息都发送到下标为1的队列 SendResult sendResult = mqProducer.send(message, new MessageQueueSelector() { @Override // select方法第一个参数: 指该Topic下有的队列集合 // 第二个参数: 发送的消息 // 第三个参数: 消息将要进入的队列下标,它与send方法的第三个参数相同 public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { return mqs.get((Integer) arg); } }, 1); System.out.println("sendResult = " + sendResult); } // 如果不再发送消息,关闭Producer实例 mqProducer.shutdown(); } }
消费者:
package org.apache.rocketmq.example.common.orderMessage; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class OrderMQConsumer { public static void main(String[] args) throws MQClientException { // 创建DefaultMQPushConsumer类并设定消费者名称 DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("consumer-group-test"); // 设置NameServer地址,如果是集群的话,使用分号;分隔开 mqPushConsumer.setNamesrvAddr("127.0.0.1:9876"); // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 // 如果不是第一次启动,那么按照上次消费的位置继续消费 mqPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息,如果订阅该主题下的所有tag,则使用* mqPushConsumer.subscribe("GLOBAL_ORDER_TOPIC", "*"); /** * 与普通消费一样需要注册消息监听器,但是传入的不再是MessageListenerConcurrently * 而是需要传入MessageListenerOrderly的实现子类,并重写consumeMessage方法。 */ // 顺序消费同一个队列的消息 mqPushConsumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { context.setAutoCommit(false); for (MessageExt msg : msgs) { System.out.println("消费线程=" + Thread.currentThread().getName() + ", queueId=" + msg.getQueueId() + ", 消息内容:" + new String(msg.getBody())); } // 标记该消息已经被成功消费 return ConsumeOrderlyStatus.SUCCESS; } }); // 启动消费者实例 mqPushConsumer.start(); } }
2、局部顺序消息
局部顺序消息就是 有顺序依赖的消息放在同⼀个queue中,多个queue并⾏消费。
⽣产者
package org.apache.rocketmq.example.common.orderMessage; import org.apache.rocketmq.client.exception.MQBrokerException; 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 org.apache.rocketmq.remoting.exception.RemotingException; import java.util.List; /*** * @description 局部顺序消息 * @author martin * @date 2024/5/22 17:25 * @param */ public class LocalOrderProducer { public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException { DefaultMQProducer defaultMQProducer = new DefaultMQProducer("local_order_producer_group"); defaultMQProducer.setNamesrvAddr("localhost:9876"); defaultMQProducer.start(); for (int i = 0; i < 10; i++) { int orderId = i; for (int j = 0; j < 5; j++) { // 构建消息体,tags和key 只是做一个简单区分 Message partOrderMsg = new Message("part_order_topic_test", "order_" + orderId, "KEY_" + orderId, ("局部顺序消息处理_" + orderId + ";step_" + j).getBytes()); SendResult send = defaultMQProducer.send(partOrderMsg, new MessageQueueSelector() { @Override //这里的arg参数就是外面的orderId public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Integer orderId = (Integer) arg; int index = orderId % mqs.size(); return mqs.get(index); } }, orderId); System.out.printf("%s%n", send); } } defaultMQProducer.shutdown(); } }
消费者
package org.apache.rocketmq.example.common.orderMessage; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class LocalOrderConsumer { public static void main(String[] args) throws MQClientException { // 创建DefaultMQPushConsumer类并设定消费者名称 DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("local_order_consume_group"); // 设置NameServer地址,如果是集群的话,使用分号;分隔开 mqPushConsumer.setNamesrvAddr("localhost:9876"); // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 // 如果不是第一次启动,那么按照上次消费的位置继续消费 mqPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息,如果订阅该主题下的所有tag,则使用* mqPushConsumer.subscribe("part_order_topic_test", "*"); /** * 与普通消费一样需要注册消息监听器,但是传入的不再是MessageListenerConcurrently * 而是需要传入MessageListenerOrderly的实现子类,并重写consumeMessage方法。 */ // 顺序消费同一个队列的消息 mqPushConsumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { context.setAutoCommit(false); for (MessageExt msg : msgs) { System.out.println("消费线程=" + Thread.currentThread().getName() + ", queueId=" + msg.getQueueId() + ", 消息内容:" + new String(msg.getBody())); } // 标记该消息已经被成功消费 return ConsumeOrderlyStatus.SUCCESS; } }); // 启动消费者实例 mqPushConsumer.start(); } }
只有当⼀组有序的消息发送到同⼀个MessageQueue上时,利⽤MessageQueue先进先出的特性,从⽽保证这⼀组消息有序
这⾥需要注意的是,MessageQueue先进先出的特性只能保证在同⼀个MessageQueue上的消息是有序的,不同MessageQueue之间的消息仍旧是乱序的。
要保证顺序消费,consumer需要按队列⼀个⼀个来取消息,即取完⼀个队列的消息后,再去取下⼀个队列的消息。
顺序消费的缺点:
-
消费顺序消息的并⾏度依赖于队列的数量 ;
-
队列热点问题,个别队列由于哈希不均导致消息过多,消费速度跟不上,产⽣消息堆积问题;
-
遇到消息失败的消息,⽆法跳过,当前队列消费暂停;
4.3、⼴播消息
⼴播消息是向主题(topic)的所有订阅者发送消息,订阅同⼀个topic的多个消费者,都能全量收到⽣产者发送的所有消息。
/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.rocketmq.example.broadcast; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.remoting.protocol.heartbeat.MessageModel; public class PushConsumer { public static final String CONSUMER_GROUP = "please_rename_unique_group_name_1"; public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; public static final String TOPIC = "TopicTest"; public static final String SUB_EXPRESSION = "TagA || TagC || TagD"; public static void main(String[] args) throws InterruptedException, MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.setMessageModel(MessageModel.BROADCASTING); consumer.subscribe(TOPIC, SUB_EXPRESSION); consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); consumer.start(); System.out.printf("Broadcast Consumer Started.%n"); } }
4.4、延迟消息
延迟消息实现的效果就是在调⽤producer.send⽅法后,消息并不会⽴即发送出去,⽽是会等⼀段时间再发送出去。 开源版本的RocketMQ中,对延迟消息并不⽀持任意时间的延迟设定(商业版本中⽀持),⽽是只⽀持18个固定的延迟级别,1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。
⽣产者:
package org.apache.rocketmq.example.common.delayMessage; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; public class DelayMessageProduct { public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException { DefaultMQProducer defaultMQProducer = new DefaultMQProducer("scheduled_product_group"); defaultMQProducer.setNamesrvAddr("localhost:9876"); defaultMQProducer.start(); for (int i = 0; i < 100; i++) { Message message = new Message("Schedule_topic", ("延迟消息测试" + i).getBytes()); //设置延迟级别,默认有18个延迟级别,这个消息将延迟10秒消费 message.setDelayTimeLevel(3); defaultMQProducer.send(message); } System.out.println("所有延迟消息发送完成"); defaultMQProducer.shutdown(); } }
消费者
package org.apache.rocketmq.example.common.delayMessage; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.consumer.ConsumeFromWhere; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class DelayConsumer { public static void main(String[] args) throws MQClientException { // 创建DefaultMQPushConsumer类并设定消费者名称 DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("scheduled_product_group"); // 设置NameServer地址,如果是集群的话,使用分号;分隔开 mqPushConsumer.setNamesrvAddr("localhost:9876"); // 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 // 如果不是第一次启动,那么按照上次消费的位置继续消费 mqPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 订阅一个或者多个Topic,以及Tag来过滤需要消费的消息,如果订阅该主题下的所有tag,则使用* mqPushConsumer.subscribe("Schedule_topic", "*"); // 顺序消费同一个队列的消息 mqPushConsumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { context.setAutoCommit(false); for (MessageExt msg : msgs) { System.out.println("消费线程=" + Thread.currentThread().getName() + ", queueId=" + msg.getQueueId() + ", 消息内容:" + new String(msg.getBody())); } // 标记该消息已经被成功消费 return ConsumeOrderlyStatus.SUCCESS; } }); // 启动消费者实例 mqPushConsumer.start(); } }
延迟消息⽣产者与普通消息⽣产者主要的区别是延迟消息需要调⽤ setDelayTimeLevel ⽅法设置延迟级别,这⾥设置级别是3,则是延迟10秒。RocketMQ提供了18种延迟级别。可以在 RocketMQ的仪表板中的集群中的
broker配置中找到。
4.5、批量消息
批量消息是指将多条消息合并成⼀个批量消息,⼀次发送出去。这样的好处是可以减少⽹络IO,提升吞吐量。
根据官⽹的注释:如果批量消息⼤于1MB就不要⽤⼀个批次发送,⽽要拆分成多个批次消息发送。
实际使⽤时,这个1MB的限制可以稍微扩⼤点,实际最⼤的限制是4194304字节,⼤概4MB。同时批量消息的使⽤是有⼀定限制的,这些消息应该有相同的Topic
/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.rocketmq.example.batch; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; public class SimpleBatchProducer { public static final String PRODUCER_GROUP = "BatchProducerGroupName"; public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876"; public static final String TOPIC = "BatchTest"; public static final String TAG = "Tag"; public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP); // Uncomment the following line while debugging, namesrvAddr should be set to your local address // producer.setNamesrvAddr(DEFAULT_NAMESRVADDR); producer.start(); //If you just send messages of no more than 1MiB at a time, it is easy to use batch //Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support List<Message> messages = new ArrayList<>(); messages.add(new Message(TOPIC, TAG, "OrderID001", "Hello world 0".getBytes(StandardCharsets.UTF_8))); messages.add(new Message(TOPIC, TAG, "OrderID002", "Hello world 1".getBytes(StandardCharsets.UTF_8))); messages.add(new Message(TOPIC, TAG, "OrderID003", "Hello world 2".getBytes(StandardCharsets.UTF_8))); SendResult sendResult = producer.send(messages); System.out.printf("%s", sendResult); } }
4.6、过滤消息
1、使⽤tag过滤
⾸先是根据tag来过滤消息,⽣产者在发送消息的时候指定该消息的tag标签,消费者则可以根据tag来过滤消息。
1.1、 过滤消息⽣产者
这⾥定义了三个tag,分别是tagA,tagB以及tagC,⽣产者在⽣产消息的时候给每个消息指定不同的tag。
package org.apache.rocketmq.example.common.filter; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; public class FilterProduct { public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException { DefaultMQProducer defaultMQProducer = new DefaultMQProducer("TagProducer_group"); defaultMQProducer.setNamesrvAddr("localhost:9876"); defaultMQProducer.start(); String[] tags = new String[]{"tagA", "tagB", "tagC"}; for (int i = 0; i < 15; i++) { Message message = new Message("TagFilterTest", tags[i % tags.length], ("tag消息过滤" + tags[i % tags.length]).getBytes()); SendResult send = defaultMQProducer.send(message); System.out.printf("%s%n", send); } defaultMQProducer.shutdown(); } }
1.2、消费者过滤
消费者过滤出了标签带有tagA以及tagC的消息进⾏消费。这⾥其实是broker将consumer需要的消息推给消费者。
消费者
package org.apache.rocketmq.example.common.filter; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; public class FilterConsume { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("tagConsumer"); defaultMQPushConsumer.setNamesrvAddr("localhost:9876"); defaultMQPushConsumer.subscribe("TagFilterTest", "tagA||tagC"); defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { for (MessageExt msg : msgs) { System.out.println("接收到的消息=" + msg); System.out.println("接收到的消息体=" + new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); defaultMQPushConsumer.start(); System.out.println("消费者已经启动"); } }
2、sql过滤
SQL 功能可以通过发送消息时输⼊的属性进⾏⼀些计算,来实现消息过滤
语法:RocketMQ只定义了⼀些基本的语法类⽀持这个特性。
-
数值⽐较:如 > , >= , <= , BETWEEN , = ;
-
字符⽐较:如 = ,'<>', IN ;
-
IS NULL 或 IS NOT NULL ;
-
逻辑 AND , OR , NOT ;
SQL过滤⽣产者
⽣产者主要设置属性过滤 message.putUserProperty("a", String.valueOf(i)); 表示第⼀条消息键值对是
a=0,第⼆条消息键值对是a=1。
⽣产者
package org.apache.rocketmq.example.common.filter; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; public class SQLFilterProduct { public static void main(String[] args) throws MQClientException, MQBrokerException, RemotingException, InterruptedException { DefaultMQProducer defaultMQProducer = new DefaultMQProducer("TagProducer_group"); defaultMQProducer.setNamesrvAddr("localhost:9876"); defaultMQProducer.start(); String[] tags = new String[]{"tagA", "tagB", "tagC"}; for (int i = 0; i < 15; i++) { String msg="sql消息过滤" + tags[i % tags.length]; Message message = new Message("SQLFilterTest", tags[i % tags.length], msg.getBytes()); message.putUserProperty("a", String.valueOf(i)); System.out.printf("%s%n", msg); SendResult send = defaultMQProducer.send(message); System.out.printf("%s%n", send); } defaultMQProducer.shutdown(); } }
SQL过滤消费者:
package org.apache.rocketmq.example.common.filter; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.MessageSelector; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; public class SQLFilterConsume { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("tagConsumer"); defaultMQPushConsumer.setNamesrvAddr("localhost:9876"); defaultMQPushConsumer.subscribe("SQLFilterTest", MessageSelector.bySql("(TAGS is not null and TAGS in ('tagA','tagC'))"+" and (a is not null and a between 0 and 3)")); defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> { for (MessageExt msg : msgs) { System.out.println("接收到的消息=" + msg); System.out.println("接收到的消息体=" + new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; }); defaultMQPushConsumer.start(); System.out.println("消费者已经启动"); } }
4.7、事务消息
事务消息是在分布式系统中保证最终⼀致性的两阶段提交的消息实现。也就是说,事务消息可以保证本地事务执⾏与消息发送两个操作的原⼦性。
4.7.1、基本实现
1、事务消息只保证消息发送者的本地事务与发消息这两个操作的原⼦性,因此,事务消息的示例只涉及到消息发送者,对于consumer来说,并没有什么区别。
package org.apache.rocketmq.example.common.transaction; import org.apache.commons.lang3.StringUtils; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.LocalTransactionState; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.client.producer.TransactionListener; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.remoting.common.RemotingHelper; import java.io.UnsupportedEncodingException; import java.util.concurrent.*; public class TransactionProduct { public static void main(String[] args) throws Exception { TransactionListener transactionListener = new TransactionListenerImpl(); TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("127.0.0.1:9876"); ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("client-transaction-msg-check-thread"); return thread; } }); producer.setExecutorService(executorService); producer.setTransactionListener(transactionListener); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; for (int i = 0; i < 10; i++) { try { Message msg =new Message("TopicTest", tags[i % tags.length], "KEY" + i,("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.sendMessageInTransaction(msg,null); System.out.printf("%s%n", sendResult); Thread.sleep(10); } catch (MQClientException | UnsupportedEncodingException e) { e.printStackTrace(); } } for (int i = 0; i < 100000; i++) { Thread.sleep(1000); } producer.shutdown(); } } class TransactionListenerImpl implements TransactionListener { //在提交完事务消息后执行。 //返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。 //返回ROLLBACK_MESSAGE状态的消息会被丢弃。 //返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。 @Override public LocalTransactionState executeLocalTransaction(Message msg,Object arg) { String tags = msg.getTags(); //TagA的消息会立即被消费者消费到 if (StringUtils.contains(tags, "TagA")) { return LocalTransactionState.COMMIT_MESSAGE; //TagB的消息会被丢弃 } else if (StringUtils.contains(tags, "TagB")) { return LocalTransactionState.ROLLBACK_MESSAGE; //其他消息会等待Broker进行事务状态回查。 } else { return LocalTransactionState.UNKNOW; } } //在对UNKNOWN状态的消息进行状态回查时执行。返回的结果是一样的。 @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { String tags = msg.getTags(); //TagC的消息过一段时间会被消费者消费到 if(StringUtils.contains(tags,"TagC")){ return LocalTransactionState.COMMIT_MESSAGE; //TagD的消息也会在状态回查时被丢弃掉 }else if(StringUtils.contains(tags,"TagD")){ return LocalTransactionState.ROLLBACK_MESSAGE; //剩下TagE的消息会在多次状态回查后最终丢弃 }else{ return LocalTransactionState.UNKNOW; } } }
4.7.2、事务消息的实现流程
事务消息的⼤致⽅案,其中分为两个流程:事务消息的发送及提交、事务消息的补偿。
第⼀阶段:事务消息的发送及提交
1.Producer向Broker服务器发送Half消息
2.Broker服务器返回Half消息的响应结果给Producer
3.Producer根据Broker的响应结果执⾏本地事务,如果响应结果为失败,则half消息对业务不可⻅,本地逻辑不执
⾏
4.Broker根据Producer本地事务状态,进⾏Commit或者Rollback(Commit操作⽣成消息索引,消息对消费者可
⻅)
第⼆阶段:事务消息的补偿
5.当Broker没有收到Producer本地事务的执⾏状态时,Broker主动发起请求,回查Producer的本地事务执⾏状态
6.Producer收到回查消息,检查回查消息对应的本地事务的状态
7.Producer根据本地事务状态,重新Commit或者Rollback
4.7.3 、事务消息设计
1、第⼀阶段事务消息对⽤户不可⻅
在RocketMQ事务消息流程中,在第⼀阶段时,消息就会发送到broker端,但是在消息提交之前,该消息对消费者是不可⻅的。即事务消息对⽤户不可⻅。
实现原理如下: 针对half消息,RocketMQ会备份该消息的主题与消费队列信息,然后将该消息topic改为RMQ_SYS_TRANS_HALF_TOPIC,消费队列置为0。 由于消费者并未订阅该主题,因此不会拉取到该消息。同时RocketMQ会开启⼀个定时任务,从Topic为RMQ_SYS_TRANS_HALF_TOPIC中拉取消息进⾏消费,根据⽣产者组获取⼀个服务提供者发送回查事务状态请求,根据事务状态来决定是提交或回滚消息
2、 Commit和Rollback操作以及Op消息的引⼊
在完成第⼀阶段写⼊⼀条对⽤户不可⻅的消息后,第⼆阶段如果是Commit操作,则需要让消息对⽤户可⻅。 由于第⼀阶段的消息对⽤户是不可⻅的,Rollback其实不需要真正撤销消息(实际上RocketMQ⽆法真正删除⼀条消息,因为是顺序写⽂件的)。但是需要⼀个操作来标识这条消息的最终状态,⽤于区别这条消息没有确定状态(Pending状态)。
RocketMQ事务消息⽅案中引⼊了Op消息的概念,⽤Op消息标识事务消息已经确定的状态(Commit或者Rollback)。如果⼀条事务消息没有对应的Op消息,说明这个事务的状态还⽆法确定。引⼊Op消息后,事务消息⽆论是Commit或者Rollback都会记录⼀个Op操作。Commit相对于Rollback只是在写⼊Op消息前创建Half消息的索引。
3、补偿机制——事务回查
如果在RocketMQ事务消息的⼆阶段过程中失败了,例如在做Commit操作时,出现⽹络问题导致Commit失败,那么需要通过⼀定的策略使这条消息最终被Commit。
RocketMQ采⽤了⼀种补偿机制,称为“回查”。Broker端对未确定状态的消息发起回查,将消息发送到对应的Producer端(同⼀个Group的Producer),由Producer根据消息来检查本地事务的状态,进⽽执⾏Commit或者Rollback。
Broker端通过对⽐Half消息和Op消息进⾏事务消息的回查并且推进CheckPoint(记录那些事务消息的状态是确定的)。
当然,rocketmq并不会⽆休⽌的的信息事务状态回查,默认回查15次,如果15次回查后还是⽆法得知事务状态,rocketmq默认回滚该消息。
4.7.4、事务消息的使⽤限制
事务消息不⽀持延迟消息和批量消息。
原因:RocketMQ 设计上不⽀持批量消息,主要原因是批量消息可能会导致消息顺序性问题和事务消息的⼀致性问题。批量消息可能会增加消息丢失的⻛险,因此 RocketMQ 选择避免批量消息。