特性
订阅与发布
消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息,进而从该topic消费数据。
1. 添加依赖
org.apache.rocketmq
rocketmq-client
4.3.0
2. 发送同步消息
消息发送后进入同步等待状态,可以保证消息投递一定到达。可靠的同步消息传输可用于重要的通知消息,SMS通知,SMS营销系统等广泛的场景中。
package com.l1fe1.rocketmq;
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;
public class SyncProducer {
public static void main(String[] args) throws Exception {
// 使用生产者组名实例化 DefaultMQProducer
DefaultMQProducer producer = new
DefaultMQProducer("producerGroup1");
// 指定 name server 的地址
producer.setNamesrvAddr("192.168.114.60:9876");
// 启动实例
producer.start();
for (int i = 0; i < 100; i ++) {
// 创建一个消息实例,指定 topic,tag和消息体
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 调用发送消息的接口将消息发送到一个 broker 上
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
// 关闭生产者实例
producer.shutdown();
}
}
3. 发送异步消息
异步传输通常用于响应时间敏感的业务场景。想要快速发送消息,又不想丢失的时候可以使用异步消息。
package com.l1fe1.rocketmq;
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 AsyncProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producerGroup1");
producer.setNamesrvAddr("192.168.114.60: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));
// 异步可靠消息
// 不会阻塞,等待 broker 确认
// 采用事件监听的方式接收 broker 返回的确认
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
}
@Override
public void onException(Throwable e) {
// 发生异常,可以尝试重投或者做额外的业务逻辑处理
countDownLatch.countDown();
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// 由于是异步发送,因此需要等一段时间再将 producer 关闭,否则发送消息会产生异常
countDownLatch.await(5, TimeUnit.SECONDS);
producer.shutdown();
}
}
4. 发送单向消息
只发送消息,不等待服务器响应,只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。单向消息适用于需要适度可靠性的场景,例如日志收集。
package com.l1fe1.rocketmq;
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("please_rename_unique_group_name");
producer.setNamesrvAddr("192.168.114.60:9876");
producer.start();
for (int i = 0; i < 100; i++) {
Message msg = new Message("TopicTest" /* Topic */,
"TagA" /* Tag */,
("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */
);
// 单向消息
// 网络不确定
producer.sendOneway(msg);
}
// 等待消息发送完成
Thread.sleep(5000);
producer.shutdown();
}
}
5. 批量消息发送
可以多条消息打包一起发送,减少网络传输次数提高效率。
producer.send(Collection c)
方法可以接收一个集合实现批量发送。
List messages = new ArrayList<>();
for (int i = 0; i < 100; i ++) {
Message msg = new Message("TopicTest", "TagA", ("Hello RocketMQ " +
i).getBytes(RemotingHelper.DEFAULT_CHARSET)
}
try {
producer.send(messages);
} catch (Exception e) {
e.printStackTrace();
}
- 批量消息要求必要具有同一topic、相同消息配置(waitStoreMsgOK)
- 不支持延时消息
- 建议一个批量消息最好不要超过1MB大小
- 如果不确定是否超过限制,可以手动计算大小分批发送
拆分成小的列表
批量发送消息的复杂度只会在发送大批量并且你可能不确定它是否超过了大小限制(1MiB)时增加。在这种情况下,你最好将列表拆分:
class ListSplitter implements Iterator> {
private final int SIZE_LIMIT = 1000 * 1000;
private final List messages;
private int currIndex;
public ListSplitter(List messages) {
this.messages = messages;
}
@Override public boolean hasNext() {
return currIndex < messages.size();
}
@Override public List next() {
int nextIndex = currIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = message.getTopic().length() + message.getBody().length;
Map properties = message.getProperties();
for (Map.Entry entry : properties.entrySet()) {
tmpSize += entry.getKey().length() + entry.getValue().length();
}
tmpSize = tmpSize + 20; // 日志开销
if (tmpSize > SIZE_LIMIT) {
// 意外情况下的单个消息大小超出了 SIZE_LIMIT 限制
// 这种情况下让它跳过,否则会阻碍拆分过程
if (nextIndex - currIndex == 0) {
// 如果下一个子列表中没有元素,则添加此元素,然后跳过,否则直接跳过
nextIndex ++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
}
List subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
}
// 将大的 list 拆分成小的 list
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
try {
List listItem = splitter.next();
producer.send(listItem);
} catch (Exception e) {
e.printStackTrace();
}
}
消费消息
package com.l1fe1.rocketmq;
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;
public class Consumer {
public static void main(String[] args) throws MQClientException {
// 使用消费者组名实例化 DefaultMQPushConsumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup1");
// 指定 name server 的地址
consumer.setNamesrvAddr("192.168.114.60:9876");
// 订阅某个 topic 进行消费,第二个参数为过滤器,* 表示不过滤
consumer.subscribe("TopicTest", "*");
// 注册回调以在从 broker 获取的消息到达时执行
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
// 默认情况下,这条消息只会被一个 consumer 消费(点到点)
// broker 端会对 message 进行状态修改
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 启动消费者实例
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
消息消费模式
消息消费模式由消费者来决定,可以由消费者设置MessageModel来决定消息消费模式。
消息模式默认为集群消费模式。
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.setMessageModel(MessageModel.CLUSTERING);
集群消息
集群消息是指集群化部署消费者
当使用集群消费模式时,MQ 认为任意一条消息只需要被集群内的任意一个消费者处理即可。
特点
- 每条消息只需要被处理一次,broker只会把消息发送给消费集群中的一个消费者
- 在消息重投时,不能保证路由到同一台机器上
- 消费状态由broker维护
广播消息
当使用广播消费模式时,MQ 会将每条消息推送给集群内所有注册过的客户端,保证消息至少被每台机器消费一次。
特点
- 消费进度由consumer维护
- 保证每个消费者消费一次消息
- 消费失败的消息不会重投
package com.l1fe1.rocketmq;
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;
public class BroadcastProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producerGroup1");
producer.setNamesrvAddr("192.168.114.60:9876");
producer.start();
for (int i = 0; i < 100; i ++){
Message msg = new Message("TopicTest",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
}
}
package com.l1fe1.rocketmq;
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.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
public class BroadcastConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup1");
consumer.setNamesrvAddr("192.168.114.60:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 设置成广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
consumer.subscribe("TopicTest", "TagA || TagC || TagD");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.printf("Broadcast Consumer Started.%n");
}
}
消息过滤
RocketMQ的消费者可以根据Tag进行消息过滤,也支持自定义属性过滤。RocketMQ支持两种消息过滤方式,一种是在Broker端进行过滤,另一种是在Consumer端进行过滤。
Broker端消息过滤
在Broker中,按照Consumer的要求做过滤,优点是减少了对于Consumer无用消息的网络传输。
缺点是增加了Broker的负担,实现相对复杂。
- 淘宝的Notify支持多种过滤方式,包含直接按照消息类型过滤,灵活的语法表达式过滤,几乎可以满足最苛刻的过滤需求。
- RocketMQ支持按照简单的Message Tag过滤,也支持按照Message Header、body进行过滤。
- CORBA Notification规范中也支持灵活的语法表达式过滤。
Consumer端消息过滤
这种过滤方式可由应用完全自定义实现,但是缺点是很多无用的消息要传输到Consumer端。
TAG过滤
在大多数情况下,TAG是一个简单而有用的设计,其可以用来选择你想要的消息。例如:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
消费者将接收包含TAGA或TAGB或TAGC的消息。但是限制是一个消息只能有一个标签,这对于复杂的场景可能不起作用。在这种情况下,可以使用SQL表达式筛选消息。
SQL表达式过滤
SQL特性可以通过发送消息时的属性来进行计算。在RocketMQ定义的语法下,可以实现一些简单的逻辑。下面是一个例子:
------------
| message |
|----------| a > 5 AND b = 'abc'
| a = 10 | --------------------> Gotten
| b = 'abc'|
| c = true |
------------
------------
| message |
|----------| a > 5 AND b = 'abc'
| a = 1 | --------------------> Missed
| b = 'abc'|
| c = true |
------------
基本语法
RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
- 数值比较,比如:>,>=,<,<=,BETWEEN,=;
- 字符比较,比如:=,<>,IN;
- IS NULL 或者 IS NOT NULL;
- 逻辑符号 AND,OR,NOT;
常量支持类型为: - 数值,比如:123,3.1415;
- 字符,比如:'abc',必须用单引号包裹起来;
- NULL,特殊的常量
- 布尔值,TRUE 或 FALSE
只有使用push模式的消费者才能用使用SQL92标准的sql语句,例如:
MessageSelector selector = MessageSelector.bySql("order > 5");
consumer.subscribe("TopicTest", selector);
配置
在broker.conf
中添加配置:
enablePropertyFilter=true
启动 broker 加载指定配置文件:
./mqbroker -n localhost:9876 -c ../conf/broker.conf
随后在集群配置中可以看到:
样例
发送消息时,你可以通过putUserProperty来设置消息的属性:
package com.l1fe1.rocketmq;
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;
public class FilterProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("producerGroup1");
producer.setNamesrvAddr("192.168.114.60:9876");
producer.start();
for (int i = 0; i < 100; i ++){
Message msg = new Message("TopicTest",
"TagA",
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
// 设置一些属性
msg.putUserProperty("a", String.valueOf(i));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
}
producer.shutdown();
}
}
可以通过MessageSelector.bySql来筛选消息
package com.l1fe1.rocketmq;
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;
public class FilterConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumerGroup1");
consumer.setNamesrvAddr("192.168.114.60:9876");
// 只有订阅的消息有这个属性a, a >=0 and a <= 3
consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3"));
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();
System.out.printf("Broadcast Consumer Started.%n");
}
}
参考资料
- RocketMQ 官网
- RocketMQ 官方文档
- 十分钟入门RocketMQ