摘自《rocketmq实战原理与解析》
生产者和消费者是消息队列的两个重要的角色,生产者向消息队列写入数据,消费者从消息队列读取数据。Rocketmq的大部分用户只需要关心生产者和消费者,本文将着重介绍生产者和消费者的特点以及它们的offset和logs
消费者可以分为两种不同的类型,一个是DefaultMQPushConsume,由系统控制读取操作,收到消息后自动调用传入的处理方法,另外一个则是DefaultMQPullConsume,读取操作中的大部分功能都是由使用者自主控制。
DefaultMqPushConsume的使用
push模式主要是这种好各种参数和传入消息的函数,系统收到消息后会自动调用处理函数来处理消息,自动保存offset,而且加入新的DefaultMqPushConsume后会自动做负载均衡,如下。
package rocketmq.day02;
import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
import com.alibaba.rocketmq.common.protocol.heartbeat.MessageModel;
import java.io.UnsupportedEncodingException;
/**
* @author heian
* @create 2019-12-09-8:12 上午
* @description
*/
public class QuickStart {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("unique_consume_group_name");
consumer.setNamesrvAddr("192.168.142.133:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.setMessageModel(MessageModel.CLUSTERING);//默认是集群模式
consumer.subscribe("topicName",null);
consumer.registerMessageListener((MessageListenerConcurrently) (listMsg, consumeConcurrentlyContext) -> {
byte[] body = listMsg.get(0).getBody();
try {
String ms = new String(body,"utf-8");
System.out.println(Thread.currentThread().getName()+"收到消息:" + ms);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
}
}
DeafultMqPushConsume 需要设置三个参数,消费者的组名consumeGroupName,地址端口NameServer, 主题名称Topic。
1)消费者的消费者组名用于把多个消费者组织到一起,从而提高并发能力,而其一般与消息模式MessageModel 配合使用,模式一般分为两种,集Clustering 和广播Boradcasting
2)集群模式下同一个消费者组名里面的消费者只消费订阅消息的一部分内容,同一个消费者组名中的消费者消费的消息总合才是订阅该topic内容的整体,从而达到负载均衡
3)广播模式下同一个消费者组名的每个消费者都能消费到所订阅topic的全部消息,也就是一个消息会被分发多次,被多个消费者消费
4)NameServer的地址端口可以填写多个,用分号隔开,从而消除单点故障 如:ip1:port;ip2:port2
4)topic的名称用来标示消息类型,需要提前创建。如果不需要消费某个topic的内容可以根据tag进行消息过滤,如果consume.subsrcibe("topicName","tag1||tag2||tag3"), 表示消费者只需要订阅此topic下的tag1 2 3的消息,如果填写*或者null则表示订阅该topic所有的消息
DefaultMqPushConsume的处理流程
DefaultMqPushConsume 主要功能实现DefaultMqPushConsumerImpl类中,消息处理逻辑是在pullMessage这个函数的PullCallback这个回调类中,这个类有个switch语句,根据broker 返回的消息类型做想用的处理,但是可以发现push 模式下为什么会使用pullRequest呢?这是通过长轮询的方式达到push的效果,长轮询既有Pull的优点,又有push的实时性。
PullCallback pullCallback = new PullCallback() {
@Override
public void onSuccess(PullResult pullResult) {
if (pullResult != null) {
pullResult =
DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(
pullRequest.getMessageQueue(), pullResult, subscriptionData);
switch (pullResult.getPullStatus()) {
case FOUND:
long prevRequestOffset = pullRequest.getNextOffset();
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
long pullRT = System.currentTimeMillis() - beginTimestamp;
DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(
pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullRT);
long firstMsgOffset = Long.MAX_VALUE;
if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
else {
firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset();
DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(
pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(),
pullResult.getMsgFoundList().size());
boolean dispathToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(//
pullResult.getMsgFoundList(), //
processQueue, //
pullRequest.getMessageQueue(), //
dispathToConsume);
if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
}
else {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
}
if (pullResult.getNextBeginOffset() < prevRequestOffset//
|| firstMsgOffset < prevRequestOffset) {
log.warn(
"[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}",//
pullResult.getNextBeginOffset(),//
firstMsgOffset,//
prevRequestOffset);
}
break;
case NO_NEW_MSG:
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case NO_MATCHED_MSG:
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case OFFSET_ILLEGAL:
log.warn("the pull request offset illegal, {} {}",//
pullRequest.toString(), pullResult.toString());
pullRequest.setNextOffset(pullResult.getNextBeginOffset());
pullRequest.getProcessQueue().setDropped(true);
DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() {
@Override
public void run() {
try {
DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(
pullRequest.getMessageQueue(), pullRequest.getNextOffset(), false);
DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest
.getMessageQueue());
DefaultMQPushConsumerImpl.this.rebalanceImpl
.removeProcessQueue(pullRequest.getMessageQueue());
log.warn("fix the pull request offset, {}", pullRequest);
}
catch (Throwable e) {
log.error("executeTaskLater Exception", e);
}
}
}, 10000);
break;
default:
break;
}
}
}
@Override
public void onException(Throwable e) {
if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("execute the pull request exception", e);
}
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
PullTimeDelayMillsWhenException);
}
}
Push 方式是server端接收到消息后,主动把消息推送给client端,实时性高。对于一个提供队列服务来说的server端来说,用push推送的方式有很多弊端。
- 加大server端的工作量,进而影响server端的性能
- client端的处理能力各不相同,client端状态不受server端控制,如果client端不能及时处理server1推过来的消息会存在各种潜在问题
Pull 方式是client端循环的从服务端拉去消息,主动权在client端手里,自己拉去到一定的消息后,处理妥当了在接着取。pull方式的问题是:
- 循环拉去消息的间隔不好设定,间隔太短就处于一个“忙等待”的状态,浪费资源。
- 每个pull的时间间隔太长,server端来消息时,有可能没有被及时处理。
长轮询的方式通过client端和server端的配合,达到既拥有pull的优点,又能达到保证实时性的目的。在impl源码中又这段,其中request.setSuspendTimeOutMills(brokerSuspendMaxTimeMills),作用是设置broker最长的阻塞时间,默认是15s,注意这个是broker在没有新的消息才会阻塞,有消息则会立刻返回。
private static final long BrokerSuspendMaxTimeMillis = 1000 * 15;
从broker的源码中(没列出)可以看出,服务端接收到请求后,如果队列没有新的消息并不急于返回,而是通过一个循环不断查看状态,每次waitForRunning 一段时间(默认5秒),然后在check。默认情况下当broker一直没有新的消息,第三次check的时候,等待时间超过了request里面的brokerSuspendMaxTimeMills,就返回空结果。在等待的过程中,broker收到了新的消息后会直接调用notifyMessageArriving 函数返回结果请求。“长轮询”的核心是broker端hold住客户端过来的请求一小段时间,在这个时间内有新消息到达,就利用现有的连接立刻返回消息给consume。”长轮询“的主动权还是掌握在consume端中,broker及时有大量的消息积压也不会主动推送给consume。
长轮询的局限性,就是hold住consume请求的时候需要占用资源,它适合消息队列这种客户端数量可控的场景中。那么可以看出不管是push模式还是pull模式,都是使用pullRequest的方式(长轮询),只不过是push模式是服务端帮客户端实现从broker中拉取消息的方法(不断轮询broker有消息立即返回,没有消息则服务端不断循环查看状态每次间隔5秒如此循环三次,也就是15秒还没有消息,则返回空结果),而pull方式则需要用户自己去实现。
DefaultMqPushConsume的流量控制
本节分析push的流量控制方法,PushConsume的核心还是pull方法,所以这种方式的客户端可以根据自身的处理速度调整获取消息的速度。因为是采用多线程处理方式实现的,流量控制方面比单线程要复杂的多。(class ConsumeMessageOrderlyService implements ConsumeMessageService )
this.consumeExecutor = new ThreadPoolExecutor(//
this.defaultMQPushConsumer.getConsumeThreadMin(),//
this.defaultMQPushConsumer.getConsumeThreadMax(),//
1000 * 60,//
TimeUnit.MILLISECONDS,//
this.consumeRequestQueue,//
new ThreadFactoryImpl("ConsumeMessageThread_"));//修改线程后缀名
Pull获取消息如果直接提交到线程池执行是很难监控和控制的,比如:如何得到消息的堆积量?如何重复处理某些消息?如何延迟处理某些消息?rocketmq 定一个快照类 processQueue来解决这些问题,在这个类运行的时候,每个message queue 都会有对应的processQueue对象,保存了这个message queue消息处理状态的快照。(processquene类的成员变量)
private final Logger log = ClientLogger.getLog();
private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock();
private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
private volatile long queueOffsetMax = 0L;
private final AtomicLong msgCount = new AtomicLong();
.......
ProcessQueue对象里保存的主要内容其实就是一个treemap和readwritelock,前者以message queue的offset作为key,以消息内通作为value保存了所有从message queue获取到的但是还未被处理的消息;读写锁控制着多个线程对treemap的并发访问。
有了processqueue 对象,流量控制就方便和灵活很多了,客户端每次pull请求前会做下面的三个判断来控制流量,如下:略
pushConsumer会判断获取但还未处理的消息的个数(线程池队列中),消息总大小,offset的跨度,任何一个值只要超过设定的大小就隔一段时间在拉取消息,从而达到流量控制目的。此外processQueue还可以辅助实现顺序消费的逻辑。
DefaultMQPullConsumer的使用
使用DefaultMQPullConsumer像使用DefaultMqPushConsume一样需要设置各种参数,写处理消息的函数,同时还需要做一些额外的事情。接下请结合org.apache.rocketmq.example.simple包中的例子源码来介绍,如下代码(https://github.com/apache/rocketmq)
package rocketmq.day03;
import com.alibaba.rocketmq.client.consumer.DefaultMQPullConsumer;
import com.alibaba.rocketmq.client.consumer.PullResult;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.common.message.MessageQueue;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author heian
* @create 2019-12-26-12:47 下午
* @description pull消费者 地址:https://github.com/apache/rocketmq/blob/master/example/src/main/java/org/apache/rocketmq/example/simple/PushConsumer.java
*/
public class PullConsume {
//保存offset
private static final Map<MessageQueue, Long> OFFSE_TABLE = new HashMap<MessageQueue, Long>();
public static void main(String[] args) throws MQClientException {
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("pull_consume_group");
consumer.setNamesrvAddr("192.168.0.102:9876");// 192.168.0.102
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicName");
for (MessageQueue mq : mqs) {
System.out.println("当前队列"+mq);
SINGLE_MQ:
while (true) {
try {
long start = System.currentTimeMillis();
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end-start));
System.out.println("拉取结果:"+pullResult);
putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
System.out.println("存储在内存:"+OFFSE_TABLE);
switch (pullResult.getPullStatus()) {
case FOUND:
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break SINGLE_MQ;
case OFFSET_ILLEGAL:
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
consumer.shutdown();
}
private static long getMessageQueueOffset(MessageQueue mq) {
Long offset = OFFSE_TABLE.get(mq);
if (offset != null)
return offset;
return 0;
}
private static void putMessageQueueOffset(MessageQueue mq, long offset) {
OFFSE_TABLE.put(mq, offset);
}
}
之前已有消息发送过来,mqAdmin控制台信息如下:(如果你的broker端没启动显示为空)
idea控制台输出信息如下:
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]
耗时:9
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8}
耗时:20989
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8}
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]
耗时:4
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=12]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12}
耗时:20005
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=12, minOffset=0, maxOffset=12, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12}
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]
耗时:3
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=8]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8}
耗时:20007
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=8, minOffset=0, maxOffset=8, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8}
当前队列MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=0]
耗时:4
拉取结果:PullResult [pullStatus=FOUND, nextBeginOffset=30, minOffset=0, maxOffset=30, msgFoundList=30]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=0]=30}
耗时:25005
拉取结果:PullResult [pullStatus=NO_NEW_MSG, nextBeginOffset=30, minOffset=0, maxOffset=30, msgFoundList=0]
存储在内存:{MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=3]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=1]=12, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=2]=8, MessageQueue [topic=topicName, brokerName=humingmingdeMacBook-Pro.local, queueId=0]=30}
可以发现当轮询各个队列时,先看内存中有没有该消息队列,没有则先存到内存,再第二次轮询存好了的消息队列,消费端像broker拉取消息时(当时没有新消息过来的,源码默认阻塞10秒 private long consumerPullTimeoutMillis = 1000 * 10),消息同样是被broker端 hold一会,然后在将消息存到内存,pull消息原理解析请参考:pull源码解析
public class DefaultMQPullConsumer 成员变量:
- brokerSuspendMaxTimeMillis 默认20s 长轮询模式下挂起的最大超时时间,在Broker端根据偏移量从存储文件中查找消息时如果返回 PULL_NOT_FOUND时,不立即返回给拉取客户端,而是交给PullRequestHoldService线程,每隔5秒再去拉取一次消息,如果找到则返回给消息拉取客户端,否则超时。
- consumerTimeoutMillisWhenSuspend :默认30s 整个消息拉取过程中,拉取客户端等待服务器响应结果的超时时间
- consumerPullTimeoutMillis :默认10s 拉消息时建立网络连接的超时时间
- messageModel :消费模式,广播、集群
- messageQueueListener : 业务消息监听器
- OffsetStore :消息消费进度管理器
- registerTopics :注册主题数
- allocateMessageQueueStrategy :队列分配器
- maxReconsumeTimes :最大消息重试次数,默认16次
(1)获取MessagQueue 并遍历
一个Topic包含多个MessagQueue,如果这个consume需要获取topic所有的消息,就要遍历所有的MessagQueue,如果有特殊情况也可以选定某些特定的MessagQueue 来读取消息
(2)维护Offsetstore
从一个MessagQueue里拉取消息的时候,需要传入offset参数,随着不断的读取消息,offset会不断增长。这个时候由用户负责把offset存储下来,根据具体的情况可以存到内存或者写入到磁盘或者数据库等
(3)根据不同的消息状态做不同的消息处理
拉取消息的请求发出后,会返回 FOUND NO_MATCHED_MSG NO_NEW_MSG OFFSET_ILLEAGER四种状态,需要根根据每个状态做不同的消息处理,比较重要的两个状态是FOUND NO_NEW_MSG,分别表示获取到消息和没有新的消息
但我觉得这上面拉取消息是不合理的,因为你拉取消息后还得在拉取一遍,直到消息返回状态为无此消息才跳出循环拉取下一条。
实际情况中可以把while true循环放到外层,达到无限循环的目的,不断循环去拉去消息队列的消息(会有新的消息)因此pullConsume需要用户自己处理遍历MessagQueue,所以pullConsume有更多的自主性和灵活性。
consume的启动 关闭流程
消息队列一般是提供一个不间断的持续性服务,consume在使用过程中如何才能更优雅的启动和关闭,确保消息不漏掉和重复消费呢?
consume分为push和pull两种方式,对于pullConsume来说使用者的主动权很高,可以根据实际情况去暂停 启动 和停止消费过程。需要注意的是offset的保存,要在程序的异常处理部分增加把offset写入磁盘方面的处理,记住了每个MessagQueue的offset,才能保证消息的准确性。
pushConsume在启动的时候会做各种配置的检查,然后连接nameserver获取topic信息,在启动的时候如果遇到异常,比如无法连接nameserver,程序仍然可以正常启动不报错。(在单机环境下可以测试下),那为什么不直接报错退出呢?这个和分布式设计有关,RocketMq集群可以有对歌nameserver broker,某个机器出了问题整体服务依然可用,所以在DefaultMQPushConsumer被设计成当发现某个连接异常时不立刻退出,而是不断尝试重新连接。可以进行这样一个测试,在DefaultMQPushConsumer正常运行的时候,手动kill调broker或者name server 过一会在启动,会发现DefaultMQPushConsumer不会出错退出,在服务恢复后可正常运行,在服务不可用的期间仅仅是在日志里报异常信息(对应的broker或者name server日志)。
如果需要在消费者启动的时候去报露错误,可以在consume.start()启动后调用,如下,其会抛出MQClientException异常(配置信息不准确或者当前服务不可用)
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("topicname");
不同类型的生产者
生产者向消息队列写数据,不同的业务场景需要生产者采取不同的策略。比如同步发送 异步发送 发送事物消息等,下面具体介绍。
package rocketmq.day01;
import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.remoting.exception.RemotingException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ProducerDemo {
public static void main(String[] args) throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("unique_producer_group__name");
producer.setNamesrvAddr("192.168.142.133:9876");
producer.start();
for (int i = 0; i < 1; i++) {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat();
String format = sdf.format(date);
Message message = new Message("topicName", String.valueOf(i),format.getBytes());
SendResult sendResult= new SendResult();
try {
sendResult = producer.send(message);//有消息返回
} catch (RemotingException |MQBrokerException | InterruptedException e) {
System.out.println("消息发送失败:" + sendResult.getMsgId());
e.printStackTrace();
}
System.out.println("key:"+i + "消息的发送结果为:" + sendResult.toString() + "消息ID为:" + sendResult.getMsgId());
}
producer.shutdown();
}
}
发送消息经历五个步骤:
- 设置producer的GroupName
- 设置InstanceName 当一个jvm需要启动多个procuder的时候,通过设置不同的InstanceName来区分,不设置的话默认使用“DEFAULT"
- 设置发送失败重试次数,当网络出现异常的时候,这个次数影响消息的重复投递次数,想要保证消息不丢,可以设置多重试
- 设置nameserver 地址
- 组装并发送
消息的发送有同步发送和异步发送两种,上面使用的是异步。消息发送返回的状态有:FLUSH_DISK_TIMEOUT FLUSH_SLAVE_TIMEOUT SLAVE_NOT_AVAILABKE SEND_OK 不同状态在不同的刷盘策略和不同的同步策略的含义是不同的。
- FLUSH_DISK_TIMEOUT:表示没有在规定的时间内完成刷盘(需要broker的刷盘策略被设置成SYNC_FLUSH才会报这个错)
- FLUSH_SLAVE_TIMEOUT:表示在主备方式下,并且broker 被设置成SYNC_MASTER方式,没有在设定时间内完成主从同步
- SLAVE_NOT_AVAILABKE:这个状态产生的场景和FLUSH_SLAVE_TIMEOUT类似,表示主备方式下,并且broker被设置成SYNC_MASTER,但是没有找到被配置成slave的broker
- SEND_OK:表示发送成功,发送成功的具体含义,比如消息是否已经被存储到磁盘?消息是否被同步到slave上?消息在slave上是否被写入磁盘?需要结合所配置的刷盘策略 主从策略来定,这个状态还可以简单的理解为,没有发生上面列出的三个问题状态就是SEND_OK
写一个高质量的生产者程序,重点在于发送结果的处理,要充分考虑各种异常,写清楚对应的处理逻辑。
(1)发送延迟消息
支持发送延迟消息,broker接受到消息后,延迟一段时间在处理,使得消息在规定的一段时间内生效,延迟消息的使用方法是创建mmessage对象时,调用setDelayTimeLevel(int level) 方法设置,目前延迟消息不支持任意设置,仅支持如下(1s/5s/10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h),比如setDelayTimeLevel(3)表示延迟10s。
message.setDelayTimeLevel(3);
(2) 自定义消息发送规则
一个topic会有多个message queue,如果使用procuder的默认配置,这个procuder会轮流像各个message queue发送消息。consume在消费消息的时候,会根据负载均衡策略,消息被分配到对应的message queue,如果不经过特定的设置,某条消息被发到哪个message queue,被哪个consume消费是未知的。
如果业务需要我们把消息发送到指定的消息队列,那该怎么办?
package rocketmq.day03;
import com.alibaba.rocketmq.client.exception.MQBrokerException;
import com.alibaba.rocketmq.client.exception.MQClientException;
import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
import com.alibaba.rocketmq.client.producer.MessageQueueSelector;
import com.alibaba.rocketmq.client.producer.SendResult;
import com.alibaba.rocketmq.common.message.Message;
import com.alibaba.rocketmq.common.message.MessageQueue;
import com.alibaba.rocketmq.remoting.exception.RemotingException;
import org.aspectj.weaver.ast.Var;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
/**
* @author heian
* @create 2019-12-30-11:24 上午
* @description 指定生产者往某一个特定的队列中发消息
*/
public class MyMessageQueueSelect implements MessageQueueSelector {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object orderKey) {
int id = Integer.parseInt(orderKey.toString());
int idMainIndex = id/100;
int size = mqs.size();
int index = idMainIndex%size;
return mqs.get(index);
}
public static void main(String[] args) throws MQClientException {
DefaultMQProducer producer = new DefaultMQProducer("unique_producer_group__name");
MyMessageQueueSelect myMessageQueueSelect = new MyMessageQueueSelect();
producer.setRetryTimesWhenSendFailed(3);//重试次数
producer.setNamesrvAddr("192.168.142.133:9876");//多个用;分割
producer.start();
for (int i = 0; i < 1; i++) {
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat();
String format = sdf.format(date);
Message message = new Message("topicName", String.valueOf(i),format.getBytes());
SendResult sendResult= new SendResult();
try {
String orderKey = "";
sendResult = producer.send(message, myMessageQueueSelect, orderKey);
} catch (RemotingException | MQBrokerException | InterruptedException e) {
System.out.println("消息发送失败:" + sendResult.getMsgId());
e.printStackTrace();
}
System.out.println("key:"+i + "消息的发送结果为:" + sendResult.toString() + "消息ID为:" + sendResult.getMsgId());
}
}
}
发送消息的时候需要把MessageQueueSelector 的对象作为参数,使用public SendResult send(Message msg,MessageQueueSelector selector,Object arg)函数发送消息即可。在MessageQueueSelector的实现中,根据需要传入orderkey参数或者根据message消息内容确定把消息发到哪个message queue上,返回被选中的message queue
生产者源码
今天测试环境出现了一个bug,看报错信息如下com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl方法中的Validators.checkMessage(msg, this.defaultMQProducer);方法
if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
"the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
}
因为我的消息体是按照1000个发送的其大小在1兆多点,而使用平安内部框架观察源码最大限制是4兆,这就很奇怪为什么我消息体没超过限制却发生了这样的报错呢?com.alibaba.rocketmq.client.producer.DefaultMQProducer
private String createTopicKey = MixAll.DEFAULT_TOPIC;
private volatile int defaultTopicQueueNums = 4;
private int sendMsgTimeout = 3000;
private int compressMsgBodyOverHowmuch = 1024 * 4;
private int retryTimesWhenSendFailed = 2;
private boolean retryAnotherBrokerWhenNotStoreOK = false;
private int maxMessageSize = 1024 * 128; //平安的这里是1024*1024*4
private boolean unitMode = false;
而且看源码发现了一个比较有意思的语法,也是在com.alibaba.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl
while(true){
lable122:{
String info;
if(times<timesTotal){
....
}
break lable122;
}
times++;
}
其实也很好理解,就是你在while里自定一个一个代码块,每次执行的时候你跳出的不是整个循环而是自己自定义的代码块,如果while中有多个代码块,二者是相互隔离的,如下:跳出的代码块不再执行
public static void main(String[] args) {
label1:
for (int i=0;i<=100;i++){
label2:
if (i>6){
System.out.println("label2="+i);
break label2;
}
if (i==10){
System.out.println("the end");
break label1;
}
label3:
if (i>8){
System.out.println("break label3");
break label3;
}
}
}
label2=7
label2=8
label2=9
break label3
label2=10
the end