前言
上一篇讲了RocketMQ的安装和简单测试了系统自带的测试,本篇将开始讲RocketMQ的api使用、消息发送方式、消费模式,消息的类型。
消息
//消息Topic
private String topic;
//消息标记 0表示非事务消息
private int flag;
//一些额外属性,消息tag,key等
private Map<String, String> properties;
//消息体
private byte[] body;
//事务消息传递到消息id
private String transactionId;
消息主要由Topic和Body构成,Body表示消息的内容,Topic是给消费分组,每条消息最大不能超过4M
笔者画一个图,希望能帮助读者理解它们之间的关系,
消费者如果订阅了Topic-A,那么他会收到Topic-A的所有消息,这里只是单纯的画个图 让大家理解它们的关系,所有的消息都保存在一个
CommitLog
中,消息会根据发送的先后顺序以append的形式追加到MappedFile中,这个后面我们在讲消息发送流程和原理的时候会详细讲。
消息类型
1.实时消息
实时消息就是我们平常发送的普通消息,producer发送消息时,会通过topic获取对应的broker和queue,然后将消息发送到对应的broker的queue中。笔者给大家画流程图方便大家理解实时实时消息的发送流程
broker启动时,就会向NameServer注册地址信息和topic信息,并定时上报最新的数据
第一步:Producer拿着topic-c去NameServer获取路由信息(能拿到对应broker的ip:port
和目标queue)
第二步:broker端接收到消息请求,找到对应的topic的queueid,持久化到对应的queue中
final DefaultMQProducer mqProducer = new DefaultMQProducer("test-group");
mqProducer.setNamesrvAddr("127.0.0.1:9876");
//启动producer
mqProducer.start();
Message syncMessage = new Message("topic-c", "hello world".getBytes());
//发送实时消息
SendResult result = mqProducer.send(syncMessage);
System.out.println("同步发送成功:"+result);
2.延迟消息
延迟消息顾名思义,就是延迟多久时间将数据投递出去,实际上就是在实时发送的时候,多设置了一下延迟时间
final DefaultMQProducer mqProducer = new DefaultMQProducer("test-group");
mqProducer.setNamesrvAddr("127.0.0.1:9876");
//启动producer
mqProducer.start();
Message syncMessage = new Message("topic-c", "hello world".getBytes());
//设置消息的延迟等级 5s后发送
syncMessage.setDelayTimeLevel(2);
//发送实时消息
SendResult result = mqProducer.send(syncMessage);
System.out.println("同步发送成功:"+result);
延迟等级对应的时间:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,这个可以通过修改broke.conf文件来进行 配置,配置项=messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,时间单位支持:s、m、h、d,分别表示秒、分、时、天; 不是特殊的业务场景,不建议去修改,因为源码中有些地方也用到了这些延迟等级,改了可能会影响RocketMQ的正常运行
第一步:Producer拿着topic-c去NameServer获取路由信息(能拿到对应broker的ip:port
和目标queue)
第二步:Producer将消息的Topic手动替换为系统的延迟Topic(SCHEDULE_TOPIC_XXXX
),根据延迟等级,放到对应的延迟队列中,并备份原来的Topic和queue
第三步:broker端接收到消息请求,将消息发送到系统队列SCHEDULE_TOPIC_XXXX
中对应的延迟等级的queue中
第四步:broker会定时去扫描延迟队列中的数据,到了触发时间会将消息投递到对应的topic和queeu中
3.事务消息
RocketMQ支持分布式最终一致性事务,我们先看发送事务消息的代码
TransactionMQProducer producer = new TransactionMQProducer("transaction-group");
producer.setNamesrvAddr("127.0.0.1:9876");
//设置事务监听器
producer.setTransactionListener(new TransactionListener() {
/**
* 执行本地事务,如果执行成功返回COMMIT_MESSAGE
* broker会将消息发送出去,
* 本地实物执行失败的话,broker会将消息删除
* @param message
* @param o
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
System.out.println("------------执行本地事务-------------");
System.out.println("message:"+new String(message.getBody()));
System.out.println("messageId:"+message.getTransactionId());
try {
//执行本地事务代码
System.out.println("try code exec");
} catch (Exception e) {
//回滚事务
return LocalTransactionState.ROLLBACK_MESSAGE;
}
//提交事务
return LocalTransactionState.ROLLBACK_MESSAGE;
}
/**
* broker长时间没收到确认信息
* 会回调接口来查看本地事务的执行情况
* @param messageExt
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
//broker长时间没收到本地事务返回的状态,会主动回调询问事务状态
System.out.println("--------------------Broker执行回调检查本地事务状态-----------------------");
System.out.println("message:"+new String(messageExt.getBody()));
System.out.println("messageId:"+new String(messageExt.getTransactionId()));
//回滚信息
//return LocalTransactionState.ROLLBACK_MESSAGE;
//等一会
//return LocalTransactionState.UNKNOW;
//事务执行成功
return LocalTransactionState.COMMIT_MESSAGE;
}
});
//启动producer
producer.start();
//发送消息(半消息)
TransactionSendResult sendResult = producer.sendMessageInTransaction(new Message("transaction-topic", "测试!这是事务消息".getBytes()), null);
System.out.println(sendResult);
//这里可能有异步回调 所以这里睡15s
TimeUnit.SECONDS.sleep(15);
producer.shutdown();
这里很容易发现,这里发送消息使用的是sendMessageInTransaction()
,这是专门用来发送事务消息的,producer还注册了一个事务监听器。我们说一下事务消息发送的逻辑,大家有个概念,后面笔者会写一篇深入讲解事务消息的文章。
第一步:设置监听器以后,调用事务消息发送的方法,并不会将消息投递到消息真正的topic中,和延迟消息一样,会发送到系统默认的
半消息Topic(RMQ_SYS_TRANS_HALF_TOPIC)中。
第二步:半消息发送完以后,会回调到executeLocalTransaction()
这个方法中,我们执行本地事务,
- 本地事务成功:返回
LocalTransactionState.COMMIT_MESSAGE
,然后将消息从半消息队列中取出来,放到消息本身的Topic队列中。 - 本地事务失败或异常:返回
LocalTransactionState.ROLLBACK_MESSAGE
,Broker收到该状态,会将消息删除掉
第三步: 如果一分钟内,Broker还未收到本地事务返回的状态,Broker开始发起询问请求,也就是回调到checkLocalTransaction()
,根据方法中判断本地事务是否执行成功。
- 事务失败:返回
LocalTransactionState.ROLLBACK_MESSAGE
,Broker收到该状态后,会将半消息删除掉 - 不确定:返回
LocalTransactionState.UNKNOW
,Broker收到该状态后,Broker会默认6s询问一次,最多询问15次 - 事务成功:返回
LocalTransactionState.COMMIT_MESSAGE
,Broker收到该状态会把消息从半消息队列移到消息本身的Topic的队列
消息过滤
消息由Topic分组以后,还可以在Topic的基础上再分,假如订单服务,下单和退款都往一个Topic下发消息,Consumer监听了该Topic,收到消息以后,分不清哪条消息是下单的,哪条消息是退款的。当然我们可以在消息Body里面添加参数来标识消息是订单还是退款,这样我们在Consumer收到消息以后,需要去判断消息体的参数,才能知道具体消息该走哪套处理逻辑,RocketMQ对消息做了一个过滤的解决方案
Tag
我们先看一段代码
final DefaultMQProducer mqProducer = new DefaultMQProducer("test-group");
mqProducer.setNamesrvAddr("127.0.0.1:9876");
//启动producer
mqProducer.start();
//添加过滤条件
Message syncMessage = new Message("testMsg","tag-a", "sync: hello world".getBytes());
//同步发送
SendResult result = mqProducer.send(syncMessage);
System.out.println("同步发送成功:"+result);
这条消息会在Message的properties属性里面添加一个TAGS=tag-a,
这样消费组在监听的时候,也只需要加过滤条件就能取到哪些想要的消息。如以下Consumer代码
DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("oneGroup");
mqPushConsumer.setNamesrvAddr("127.0.0.1:9876");
//并发消费
mqPushConsumer.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;
}
});
//监听所有消息使用符号* 多个条件使用|| eg: tag-a || tag-b
mqPushConsumer.subscribe("testMsg","tag-a");
mqPushConsumer.start();
TimeUnit.SECONDS.sleep(5);
mqPushConsumer.shutdown();
这样,消费组只会收到testMsg下所有带有tag-a标签的消息。笔者画一个图方便读者理解
Sql表达式过滤
RocketMQ除了支持Tag来过滤消息,还支持更复杂的过滤方式 ,不过这样过滤方式默认是未开启
的,需要在broker.conf文件中添加该属性
enablePropertyFilter=true
先看Producer代码
final DefaultMQProducer mqProducer = new DefaultMQProducer("test-group");
mqProducer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
//发送50条消息,每条消息设置一个num属性,consumer可以根据这个属性来进行过滤
for (int i = 1; i <= 50; i++) {
Message message = new Message("testMsg","key"+i,("batch message no:"+i).getBytes());
message.putUserProperty("num",String.valueOf(i));
producer.send(message);
}
Producer发向testMsg的Topic中发送50条消息,消息内容会把当前是第几条消息标识出来,并未每条消息添加了一个自定义属性num,num的值就是Consumer来过滤的条件值。
支持过滤的语法:
- 数字比较, 像
>
,>=
,<
,<=
,BETWEEN
,=
; - 字符比较, 像
=
,<>
,IN
; -
IS NULL
或者IS NOT NULL
; - 逻辑运算
AND
,OR
,NOT
;
Consumer端代码
DefaultMQPushConsumer mqPushConsumer = new DefaultMQPushConsumer("oneGroup");
mqPushConsumer.setNamesrvAddr("127.0.0.1:9876");
//过滤器
MessageSelector selector = MessageSelector.bySql("num > 16 and num < 30");
consumer.subscribe("testMsg",selector);
//注册消息监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.println("customer received: " +new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
TimeUnit.SECONDS.sleep(10);
Consumer端监听了testMsg这个Topic,并添加了一个sql表达式的消息选择器,条件为num大于 16 小于 30,有了这个选择器,Consumer就只能收到num 大于16和小于30之间的消息。
消息发送方式
1.同步发送
上面的示例代码,都是用的同步的方式发送的,消息发送进入同步等待状态,确保消息一定发送成功。
2.异步发送
需要设置一个回调函数,消息发送过程中,异步发送的线程池另起了一个新的线程,实际调的也是同步发送的接口,等消息发送成功的时候会回调到设置的回调方法中。
//消息异步发送
mqProducer.send(asyncMessage, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("异步发送成功:"+ sendResult);
}
@Override
public void onException(Throwable e) {
System.out.println("异步发送异常:"+ e);
}
});
3.单向发送(OneWay)
单向消息和同步消息唯一不同的地方就是,同步发送关注发送的结果,单向消息不关心发送的结果,成功失败都不管。
//单向消息
mqProducer.sendOneway(oneWayMessage);
4.批量发送
消息可以放到一个集合里面,(延迟消息不支持批量)一个请求将这些消息全部发送出去,一次请求减少了网络开销。
//批量发送
mqProducer.send(Arrays.asList(syncMessage,asyncMessage,oneWayMessage));
消息重投机制
只有同步发送
才会进行重投机制,并且还要打开下面这个属性才行,默认失败重新投递2次
//打开失败重新投递
mqProducer.setRetryAnotherBrokerWhenNotStoreOK(true);
offset
消息发送以后都是存储在Message Queue,Message Queue是一个无限长的数组,offset就是它的下标,一条消息存到Message Queue中,该Message Queue的offset就要累加1,消费时就是通过offset来快速定位到具体的消息。
对应的控制界面上的数据
消费模式
消息的消费由消费者来确定,消费者支持两种消费方式,一种是集群消费、还有一种广播消费,下面我们来讲一下集群消费和广播消费的区别
集群消费
//消费模式 默认 集群消费
mqPushConsumer.setMessageModel(MessageModel.CLUSTERING);
消费者默认的消费模式,同一个消费者组,一条消息只会被其中一个Consumer消费,消费的offset由Broker维护,消费失败会重新投递
假设Producer给Broker的名为testMsg的Topic发了发两条消息 msg1 和msg2,有两个不同的消费者集群(同一个group为一个集群)监听了testMsg这个Topic,那么每条消息会被每个集群的一个Consumer消费。
广播消费
mqPushConsumer.setMessageModel(MessageModel.BROADCASTING);
如果消费者设置消费模式为广播消费的所有的消费者都会收到该消息,消费的offset由Consumer自己维护,而且消费失败Broker还不会重新投递
消费组集群和上面的集群消费一样,两条消息,会被所有的消费者消费一次。
总结
本篇主要讲了消息类型,分为实时消息、延迟消息、事务消息。还说了消息通过 tags和sql来进行过滤。以及消息的三种发送方式,同步、异步、单向。还说了消息重投的机制以及offset的概念。消费端的消费模式。消费模式主要分为了集群消费和广播消费。下一篇开始讲RocketMQ整合SpringBoot以及顺序消息。