RocketMQ详解一
- MQ(message queue)
消息队列,现在市场上普遍流行大面积都在使用的中间件。
前言
- Rocketmq,一款分布式队列模型,由阿里研发,参考Kafka并结合阿里实际业务需求的消息中间件。
一、RocketMQ简介
1、优势
RocketMQ既然能留在市面广泛的被使用,那他肯定有很多被大家认可的地方,所以先来说说他的优点:
- 强调集群无单点,可扩展,任意一点高可用,水平可扩展
- 海量消息堆积能力,消息堆积后,写入低延迟
- 支持上万个队列(没测试过)
- 消息失败重试机制
- 消息可查询
- 开源社区活跃
- 另外RocketMQ既然能受住双十一的考验,那么相信它的成熟度也达到了一定的成熟度,哈哈哈哈哈,一句废话。
但是,RocketMQ和其他MQ产品有同样的问题,用到了他的优点,同样带来了其他的问题,比如增加系统复杂性等的问题,所以这里对他的缺点不在进行描述,有兴趣的可以后面再做了解。
2、RocketMQ角色
本节先对下文中用到的专业名词进行简单的描述
1.Name Server
- Name Server 为 producer 和 consumer 提供路由信息。
2.Broker
- Broker 是 RocketMQ 系统的主要角色,其实就是前面一直说的 MQ。Broker接收来自生产者的消息,储存以及为消费者拉取消息的请求做好准备。
3.Topic
- Topic 是一种消息的逻辑分类,比如说你有订单类的消息,也有库存类的消息,那么就需要进行分类,一个是订单 Topic
存放订单相关的消息,一个是库存 Topic 存储库存相关的消息。
4.Tag
- 标签简单的来说就是Topic的细化,一般在业务场景中通过引入标签来标记不同用途的消息。
5.Message
- Message 是消息的载体。一个 Message 必须指定 topic,相当于寄信的地址。Message 还有一个可选的 tag
设置,以便消费端可以基于 tag 进行过滤消息。
6.Producer
- 消息生产者,生产者的作用就是将消息发送到
MQ,生产者本身既可以产生消息,如读取文本信息等。也可以对外提供接口,由外部应用来调用接口,再由生产者将收到的消息发送到 MQ。
7.Consumer
- 消息消费者,简单来说,消费 MQ 上的消息的应用程序就是消费者,至于消息是否进行逻辑处理,还是直接存储到数据库等取决于业务需要。
配图更易理解几者的关系
二、执行流程
1、工作流程以及原理
废话不多说,上图
做图功底不行,凑合看吧,将就一下~
下面我们逐个讲解:
1.Producer
- Producer既然做为生产者,那他的职责肯定就是负责消息的发送,看源码知道发送消息的入口在DefaultMQProducer类的send()方法,DefaultMQProducer提供了很多重载的方法,这些重载方法,内部调用了DefaultMQProducerImpl内的send方法,发送的核心逻辑就在该类的sendDefaultImpl()方法内。
源码:
private SendResult sendDefaultImpl(Message msg, CommunicationMode communicationMode, SendCallback sendCallback, long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK();
Validators.checkMessage(msg, this.defaultMQProducer);
long invokeID = this.random.nextLong();
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev = beginTimestampFirst;
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
MessageQueue mq = null;
Exception exception = null;
SendResult sendResult = null;
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
while(true) {
label129: {
String info;
if (times < timesTotal) {
info = null == mq ? null : mq.getBrokerName();
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, info);
if (mqSelected != null) {
mq = mqSelected;
brokersSent[times] = mqSelected.getBrokerName();
long endTimestamp;
try {
beginTimestampPrev = System.currentTimeMillis();
if (times > 0) {
msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
}
long costTime = beginTimestampPrev - beginTimestampFirst;
if (timeout >= costTime) {
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
switch(communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC:
if (sendResult.getSendStatus() == SendStatus.SEND_OK || !this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
return sendResult;
}
default:
break label129;
}
}
callTimeout = true;
} catch (RemotingException var26) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, true);
this.log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mqSelected), var26);
this.log.warn(msg.toString());
exception = var26;
break label129;
} catch (MQClientException var27) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, true);
this.log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mqSelected), var27);
this.log.warn(msg.toString());
exception = var27;
break label129;
} catch (MQBrokerException var28) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, true);
this.log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mqSelected), var28);
this.log.warn(msg.toString());
exception = var28;
switch(var28.getResponseCode()) {
case 1:
case 14:
case 16:
case 17:
case 204:
case 205:
break label129;
default:
if (sendResult != null) {
return sendResult;
} else {
throw var28;
}
}
} catch (InterruptedException var29) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mqSelected.getBrokerName(), endTimestamp - beginTimestampPrev, false);
this.log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mqSelected), var29);
this.log.warn(msg.toString());
this.log.warn("sendKernelImpl exception", var29);
this.log.warn(msg.toString());
throw var29;
}
}
}
if (sendResult != null) {
return sendResult;
}
info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s", times, System.currentTimeMillis() - beginTimestampFirst, msg.getTopic(), Arrays.toString(brokersSent));
info = info + FAQUrl.suggestTodo("http://rocketmq.apache.org/docs/faq/");
MQClientException mqClientException = new MQClientException(info, (Throwable)exception);
if (callTimeout) {
throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
}
if (exception instanceof MQBrokerException) {
mqClientException.setResponseCode(((MQBrokerException)exception).getResponseCode());
} else if (exception instanceof RemotingConnectException) {
mqClientException.setResponseCode(10001);
} else if (exception instanceof RemotingTimeoutException) {
mqClientException.setResponseCode(10002);
} else if (exception instanceof MQClientException) {
mqClientException.setResponseCode(10003);
}
throw mqClientException;
}
++times;
}
} else {
List<String> nsList = this.getmQClientFactory().getMQClientAPIImpl().getNameServerAddressList();
if (null != nsList && !nsList.isEmpty()) {
throw (new MQClientException("No route info of this topic, " + msg.getTopic() + FAQUrl.suggestTodo("http://rocketmq.apache.org/docs/faq/"), (Throwable)null)).setResponseCode(10005);
} else {
throw (new MQClientException("No name server address, please set it." + FAQUrl.suggestTodo("http://rocketmq.apache.org/docs/faq/"), (Throwable)null)).setResponseCode(10004);
}
}
}
- 根据源码可以看到先对生产者和消息进行判断
this.makeSureStateOK();
Validators.checkMessage(msg, this.defaultMQProducer);
- 之后调用了DefaultMQProducerImpl.tryToFindTopicPublishInfo()方法
private TopicPublishInfo tryToFindTopicPublishInfo(String topic) {
TopicPublishInfo topicPublishInfo = (TopicPublishInfo)this.topicPublishInfoTable.get(topic);
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = (TopicPublishInfo)this.topicPublishInfoTable.get(topic);
}
if (!topicPublishInfo.isHaveTopicRouterInfo() && !topicPublishInfo.ok()) {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = (TopicPublishInfo)this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
} else {
return topicPublishInfo;
}
}
- 将从NameServer获取到的Topic路由信息(TopicRouteData)封装成TopicPublishInfo,然后根据你传入的指定Topic获取路由信息,如果获取不到,可以看到调用了MQClientInstance.updateTopicRouteInfoFromNameServer()方法来进行再次获取。
源码
public boolean updateTopicRouteInfoFromNameServer(String topic, boolean isDefault, DefaultMQProducer defaultMQProducer) {
try {
if (!this.lockNamesrv.tryLock(3000L, TimeUnit.MILLISECONDS)) {
this.log.warn("updateTopicRouteInfoFromNameServer tryLock timeout {}ms", 3000L);
} else {
try {
TopicRouteData topicRouteData;
if (isDefault && defaultMQProducer != null) {
topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(), 3000L);
if (topicRouteData != null) {
Iterator var5 = topicRouteData.getQueueDatas().iterator();
while(var5.hasNext()) {
QueueData data = (QueueData)var5.next();
int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
data.setReadQueueNums(queueNums);
data.setWriteQueueNums(queueNums);
}
}
} else {
topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 3000L);
}
if (topicRouteData == null) {
this.log.warn("updateTopicRouteInfoFromNameServer, getTopicRouteInfoFromNameServer return null, Topic: {}", topic);
return false;
}
TopicRouteData old = (TopicRouteData)this.topicRouteTable.get(topic);
boolean changed = this.topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
this.log.info("the topic[{}] route info changed, old[{}] ,new[{}]", new Object[]{topic, old, topicRouteData});
}
if (!changed) {
return false;
}
TopicRouteData cloneTopicRouteData = topicRouteData.cloneTopicRouteData();
Iterator var8 = topicRouteData.getBrokerDatas().iterator();
while(var8.hasNext()) {
BrokerData bd = (BrokerData)var8.next();
this.brokerAddrTable.put(bd.getBrokerName(), bd.getBrokerAddrs());
}
TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
publishInfo.setHaveTopicRouterInfo(true);
Iterator it = this.producerTable.entrySet().iterator();
Entry entry;
while(it.hasNext()) {
entry = (Entry)it.next();
MQProducerInner impl = (MQProducerInner)entry.getValue();
if (impl != null) {
impl.updateTopicPublishInfo(topic, publishInfo);
}
}
Set<MessageQueue> subscribeInfo = topicRouteData2TopicSubscribeInfo(topic, topicRouteData);
it = this.consumerTable.entrySet().iterator();
while(it.hasNext()) {
entry = (Entry)it.next();
MQConsumerInner impl = (MQConsumerInner)entry.getValue();
if (impl != null) {
impl.updateTopicSubscribeInfo(topic, subscribeInfo);
}
}
this.log.info("topicRouteTable.put. Topic = {}, TopicRouteData[{}]", topic, cloneTopicRouteData);
this.topicRouteTable.put(topic, cloneTopicRouteData);
boolean var24 = true;
return var24;
} catch (Exception var16) {
if (!topic.startsWith("%RETRY%") && !topic.equals("TBW102")) {
this.log.warn("updateTopicRouteInfoFromNameServer Exception", var16);
}
return false;
} finally {
this.lockNamesrv.unlock();
}
}
} catch (InterruptedException var18) {
this.log.warn("updateTopicRouteInfoFromNameServer Exception", var18);
}
return false;
}
- 该方法是通过MQClientAPIImpl.getTopicRouteInfoFromNameServer()方法获取路由信息。
源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.apache.rocketmq.client.impl.producer;
import java.util.ArrayList;
import java.util.List;
import org.apache.rocketmq.client.common.ThreadLocalIndex;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.route.QueueData;
import org.apache.rocketmq.common.protocol.route.TopicRouteData;
public class TopicPublishInfo {
private boolean orderTopic = false;
private boolean haveTopicRouterInfo = false;
private List<MessageQueue> messageQueueList = new ArrayList();
private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();
private TopicRouteData topicRouteData;
public TopicPublishInfo() {
}
public boolean isOrderTopic() {
return this.orderTopic;
}
public void setOrderTopic(boolean orderTopic) {
this.orderTopic = orderTopic;
}
public boolean ok() {
return null != this.messageQueueList && !this.messageQueueList.isEmpty();
}
public List<MessageQueue> getMessageQueueList() {
return this.messageQueueList;
}
public void setMessageQueueList(List<MessageQueue> messageQueueList) {
this.messageQueueList = messageQueueList;
}
public ThreadLocalIndex getSendWhichQueue() {
return this.sendWhichQueue;
}
public void setSendWhichQueue(ThreadLocalIndex sendWhichQueue) {
this.sendWhichQueue = sendWhichQueue;
}
public boolean isHaveTopicRouterInfo() {
return this.haveTopicRouterInfo;
}
public void setHaveTopicRouterInfo(boolean haveTopicRouterInfo) {
this.haveTopicRouterInfo = haveTopicRouterInfo;
}
public MessageQueue selectOneMessageQueue(String lastBrokerName) {
if (lastBrokerName == null) {
return this.selectOneMessageQueue();
} else {
int index = this.sendWhichQueue.getAndIncrement();
for(int i = 0; i < this.messageQueueList.size(); ++i) {
int pos = Math.abs(index++) % this.messageQueueList.size();
if (pos < 0) {
pos = 0;
}
MessageQueue mq = (MessageQueue)this.messageQueueList.get(pos);
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
return this.selectOneMessageQueue();
}
}
public MessageQueue selectOneMessageQueue() {
int index = this.sendWhichQueue.getAndIncrement();
int pos = Math.abs(index) % this.messageQueueList.size();
if (pos < 0) {
pos = 0;
}
return (MessageQueue)this.messageQueueList.get(pos);
}
public int getQueueIdByBroker(String brokerName) {
for(int i = 0; i < this.topicRouteData.getQueueDatas().size(); ++i) {
QueueData queueData = (QueueData)this.topicRouteData.getQueueDatas().get(i);
if (queueData.getBrokerName().equals(brokerName)) {
return queueData.getWriteQueueNums();
}
}
return -1;
}
public String toString() {
return "TopicPublishInfo [orderTopic=" + this.orderTopic + ", messageQueueList=" + this.messageQueueList + ", sendWhichQueue=" + this.sendWhichQueue + ", haveTopicRouterInfo=" + this.haveTopicRouterInfo + "]";
}
public TopicRouteData getTopicRouteData() {
return this.topicRouteData;
}
public void setTopicRouteData(TopicRouteData topicRouteData) {
this.topicRouteData = topicRouteData;
}
}
- 可以看到此时的路由信息TopicPublishInfo中包含的数据,包括队列数据、Broker数据和Broker地址等信息,再次回到源码继续了解
源码
TopicRouteData old = (TopicRouteData)this.topicRouteTable.get(topic);
boolean changed = this.topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
this.log.info("the topic[{}] route info changed, old[{}] ,new[{}]", new Object[]{topic, old, topicRouteData});
}
if (!changed) {
return false;
}
- 看到MQClientInstance.updateTopicRouteInfoFromNameServer()方法:判断MQClientInstance.topicRouteTable属性以前缓存的路由信息是否需要更新、DefaultMQProducerImpl.topicPublishInfoTable属性以前的缓路由信息是否需要更新,如果需要则进行更新。此时我们已经选择好了Topic路由,接下来就该选择我们的目标队列了,将消息发送到那个队列中。根据源码我们看到调用了DefaultMQProducerImpl.selectOneMessageQueue()方法,
源码
public MessageQueue selectOneMessageQueue(TopicPublishInfo tpInfo, String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
int index = tpInfo.getSendWhichQueue().getAndIncrement();
int i = 0;
while(true) {
int writeQueueNums;
MessageQueue mq;
if (i >= tpInfo.getMessageQueueList().size()) {
String notBestBroker = (String)this.latencyFaultTolerance.pickOneAtLeast();
writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
}
this.latencyFaultTolerance.remove(notBestBroker);
break;
}
writeQueueNums = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (writeQueueNums < 0) {
writeQueueNums = 0;
}
mq = (MessageQueue)tpInfo.getMessageQueueList().get(writeQueueNums);
if (this.latencyFaultTolerance.isAvailable(mq.getBrokerName()) && (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))) {
return mq;
}
++i;
}
} catch (Exception var7) {
log.error("Error occurred when selecting message queue", var7);
}
return tpInfo.selectOneMessageQueue();
} else {
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
}
-
此时就需要分情况讨论了,this.sendLatencyFaultEnable判断,如果开启延迟容错策略,则要考虑Broker的响应时间。
-
延时容错策略简单的来说,就是选择broker时,如果broker响应时间长,就应该选其他的,如果响应时间达到了一定的标准,就在一段时间内,不去选择则个队列座位目标队列。
接下来分情况讨论:
(1)、开启延时容错策略:
if (this.sendLatencyFaultEnable) {
try {
int index = tpInfo.getSendWhichQueue().getAndIncrement();
int i = 0;
while(true) {
int writeQueueNums;
MessageQueue mq;
if (i >= tpInfo.getMessageQueueList().size()) {
String notBestBroker = (String)this.latencyFaultTolerance.pickOneAtLeast();
writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
}
this.latencyFaultTolerance.remove(notBestBroker);
break;
}
writeQueueNums = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (writeQueueNums < 0) {
writeQueueNums = 0;
}
mq = (MessageQueue)tpInfo.getMessageQueueList().get(writeQueueNums);
if (this.latencyFaultTolerance.isAvailable(mq.getBrokerName()) && (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))) {
return mq;
}
++i;
}
} catch (Exception var7) {
log.error("Error occurred when selecting message queue", var7);
}
return tpInfo.selectOneMessageQueue();
}
- 先是遍历了topic路由信息中的所有队列,然后队列对应的Broker状态,如果健康,则直接返回该队列;如果遍历了所有队列后,依然没有找到比较合适的Broker,则从三个方面进行选取,哪个broker更健康则优先,哪个响应时间段优先,哪个不可用时间越小优先。
(2)、关闭延迟容错策略
源码
public MessageQueue selectOneMessageQueue(String lastBrokerName) {
if (lastBrokerName == null) {
return this.selectOneMessageQueue();
} else {
int index = this.sendWhichQueue.getAndIncrement();
for(int i = 0; i < this.messageQueueList.size(); ++i) {
int pos = Math.abs(index++) % this.messageQueueList.size();
if (pos < 0) {
pos = 0;
}
MessageQueue mq = (MessageQueue)this.messageQueueList.get(pos);
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
return this.selectOneMessageQueue();
}
}
-
如果没有开启延迟容错策略,就比较简单了,直接从Topic路由信息获取,第一次随机,后续往后选另一个Broker的队列。
至此,就选择好了目标队列,然后设置一些参数就可以发送你的消息到目标broker上了。
对于发送消息,有几种类型,简单的做下介绍: -
普通消息:
同步发送:Producer发出⼀条消息后,会在收到MQ返回的ACK之后才发下⼀条消息。该方式的消息可靠性最高,但消息发送效率太低。
异步发送:Producer发出消息后无需等待MQ返回ACK,直接发送下⼀条消息。
单向发送:Producer仅负责发送消息,不等待、不处理MQ的ACK。该发送方式时MQ也不返回ACK。该方式的消息发送效率最高,但消息可靠性较差。 -
顺序消息:
顺序消息指的是,严格按照消息的发送顺序进行消费的消息(FIFO)。 -
延时消息:
当消息写入到Broker后,在指定的时长后才可被消费处理的消息,称为延时消息。 -
事务消息:
既然牵扯到了事务两个字,那么发送消息事件和其他事件需要同时成功或同时失败。
具体流程:
2.Consumer
Consumer消费者,既然是消费者,那么他的职责就是消费消息。想要消费消息,那就要先获取到消息吧,所以我们先来聊聊获取消息的两种模式:
获取消息的类型:
(1)、拉取式 pull
- Consumer主动的从Broker中拉取到消息,Consumer掌握了主动权,一旦获取到了消息,就会启动消费过程,开始消费。
(2)、推送式 push
- Broker在收到生产者发送的消息后会主动的推送给Consumer,然后进行消息消费。
Push就是典型的发布-订阅模式,即Consumer向其关联的Queue注册了监听器,一旦发现有新的消息,就会触发回调的执行,这里的回调就是Consumer去Queue中获取消息。但是这种都是基Consumer和Broker的长连接,维护长连接就需要消耗系统资源。
两种方式的利弊:
pull: - 需要实现对关联Queue的遍历,且拉取的时间间隔由用户指定的,所以他的时效性较差;但是便于应用控制消息的拉取。
push: - 封装了对关联Queue的遍历,时效性好;但同样长连接等的维护相对而言更加的耗费资源。
既然获取到了消息,那么接下来接着聊聊消息消费的两种模式:
(1)、广播模式
- 广播模式下,相同的Consumer实例都会接收到同一个Topic的全量消息。即每条消息都会被发送到消费者组中的每个Consumer。
但是这里有一点需要注意,虽然每个Consumer接收到的消息相同,但不代表他们的消费进度一样,比如:队列中有有个消息,有三个Consumer实例,有可能第一个Consumer消费完了,第二个Consumer才消费到第三个消息,所以消费进度由Consumer的消费能力决定,并不是统一的。
(2)、集群模式
- 集群模式下,相同消费者组中的每个Consumer实例会平均分摊同一个Topic的消息。也就是说每个消息只会被发送到某个Consumer。
这两种消费模式看起来的差别只是一个可以被多个Consumer消费,另一个只被一个Consumer消费吗?
- 广播模式下,消费进度保存在了Consumer端,因为他们的消费进度不同,所以各自的Consumer保存各自的进度。
- 集群模式下,消费进度保存在了Broker中,消费者组中的所有Consumer共同消费一个Topic中的消息,同一个消息只会被消费一次,那么他的消费进度会参与到消费的负载均衡中(前面说的平均分摊),所以消费进度是要共享的,所以保存在了Broker中。
此时获取到了消息,进行消费,这块儿源码太多了,就不贴源码了哈。
消息的消费和消息堆积
- Consumer获取到消息,先将Consumer本地缓存的消息提交到消费线程中,使用业务代码进行消息的消费,说白了你的业务逻辑就是真正的消费过程。刚才我们提到了Consumer的消费能力,那么此时的消费能力就完全的依赖于消费消息的耗时和消费并发度。如果消费过程或者说消费逻辑代码中,业务逻辑很复杂或者bug啥的,耗时较长,那么整体的消息吞吐量肯定很低,至少不会高。所以就会导致Consumer本地的缓存队列达到上限,停止获取消息。最终导致了消息的堆积。
避免消息堆积
刚才讲了什么场景会导致消息的堆积,主要有消费耗时和消息消费的并发度,那么接下来就这两点聊聊如何避免消息的堆积。
(1)、消费耗时
消费耗时,说白了就是你的消费业务逻辑代码的耗时,可以分为两种情况来讨论:1.CPU耗时。2。IO的耗时。
所以我们主要关注下面几点 - 消费消息的逻辑计算和执行复杂度是否过高,代码是否存在可避免的循环等等;
- 消费逻辑中的I/O操作是否是必须的,能否用本地缓存啥的来代替;
- 消费逻辑中的步骤能否做异步处理,比如发邮件,在合理的情况下可以做异步处理,由另外的线程去处理,从而避免耗时。
(2)、消费并发度
并发,对于这两个字眼并不陌生,所以对于消息并发度一下几个建议去优化: - 逐步调大单个Consumer的线程数,观察检测数据,得到最优的线程数和吞吐量;
- 根据上下游系统的流量峰值来估算出需要的节点数。
3.消息
既然消息要被Consumer消费,那消息也会分为多个种类来被消费,以便适用于不同的场景。
1、普通消息
2、顺序消息
- 顺序消息就是严格按照消息的发送顺序来进行消费的消息。消费消息时从多个Queue获取消息,这种情况无法保证消息发送和消费的顺序。但是如何将消息仅发送到同一个Queue,就严格保证了消息的顺序性。比如网购订单的下单->支付->发货等流程,就能用到该类消息。
3、延时消息
延时消息,就是消息到达Broker中后,在指定的时间后才可能被消费的消息。比如:电商采购中超时没有进行支付的场景;12306订票超时未支付等等都能用到。
重试机制
- 消息被消费,如果消费失败,可以根据状态和返回值进入重试队列。可以设置重试的间隔时间和重试次数,来进行消息的重试消费。如果超过了重试的次数,就会进入死信队列。死信队列即达到最大消费次数后,仍然消费失败,此时就说明消费者在正常情况下,是无法消费掉这条消息,这时,并不会将消息丢弃掉,而是将该消息放入一个特殊的队列中,这就是死信队列。
- 前面说了消费这如果消费消息失败,可以根据状态和返回值来判断是否需要进入重试队列,下面就说下重试机制的实现。
(1)、return null;
(2)、throw new RuntimeException(“消费异常了——喂”);
(3)、return ConsumeConcurrentlyStatus.RECONSUME_LATER;
好了,此篇文章对于大致的RocketMQ角色讲解就到此为止了,对于RocketMQ的配置和其他的一些常见的问题将在不久后和大家见面,return “拜拜”;
总结
-
作为技术型文章,没有华丽的语言和生动的文字,有的仅仅是数行代码和对其枯燥的描述,希望本文能对即将开始写作的你,有所帮助。
-
本文为作者原创文章,未经原创作者同意不得转载