RocketMQ对于消费者端顺序消费来说只能保证局部顺序,并不能保证全局顺序消费,局部顺序的意思就是只能对于一个mq的消息达到顺序消费,所以若是想要达到全局顺序消费的效果,对于一个topic来说可以值设置一个mq。
RocketMQ实现顺序消费的原理:因为要保证一个mq的消息能够被顺序消费,第一首先这个mq的消息必须要保证不能被一个以上的消费者所消费(并发消费时如果发生mq的负载均衡就可能会产生一个mq的消息被重复消费了,并且还是乱序消费的),那么其实问题就在于消费者之间是不同的系统而产生的,按照正常的逻辑去想就可以给需要被顺序消费的mq加上分布式锁,而RocketMQ也是这样解决的,不过它并没有使用其他的如redis这些第三方中间件去实现redis锁,而是直接在broker中加锁(因为消费者与broker之间就是分布式的,所以broker可以直接充当redis这些角色去给mq加分布式锁);第二,在第一点的基础上可以保证了一个mq的消息只能被一个消费者所消费,那么消费者自己消费的时候也必须要保证是顺序的,所以在顺序消费的消费线程池中一个线程值对应消费一个mq的消息
消费者对分配到的mq加上broker锁
org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalance
我们来到mq产生负载均衡的时候的代码逻辑,部分代码如下:
for (MessageQueue mq : mqSet) {
// 条件成立: 说明这个mq是新分配给当前消费者的
if (!this.processQueueTable.containsKey(mq)) {
// 如果当前消费者实例是顺序消费,那么就会先对新分配到的mq进行broker端加锁,如果加锁不成功,直接跳过
if (isOrder && !this.lock(mq)) {
log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
continue;
}
// 内存中可能有该队列的一些脏数据,所以要把这些脏数据移除
this.removeDirtyOffset(mq);
// 给新分配的mq创建一个对应的队列快照对象
ProcessQueue pq = new ProcessQueue();
// 根据用户设置的ConsumeFromWhere去获取新分配的mq下一次起始消费偏移量,ConsumeFromWhere根据setConsumeFromWhere()方法进行设置
long nextOffset = this.computePullFromWhere(mq);
// 如果起始消费偏移量 >= 0, 就创建拉取消息的任务
if (nextOffset >= 0) {
ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
if (pre != null) {
log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
} else {
log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
PullRequest pullRequest = new PullRequest();
pullRequest.setConsumerGroup(consumerGroup);
pullRequest.setNextOffset(nextOffset);
pullRequest.setMessageQueue(mq);
pullRequest.setProcessQueue(pq);
pullRequestList.add(pullRequest);
changed = true;
}
}
// 如果起始消费偏移量 < 0,就什么都不做,只打印个日志
else {
log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
}
}
}
在消费者获取到新分配的mq之后,如果这个消费者是顺序消费的,那么就需要对这个mq进行加broker锁,具体加锁的代码如下:
/**
* 当前消费者实例对目标mq进行加锁
* @param mq 目标mq
* @return true表示加broker锁成功,反之不成功
*/
public boolean lock(final MessageQueue mq) {
// 找到master节点的地址
FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true);
if (findBrokerResult != null) {
// 加锁请求体对象
LockBatchRequestBody requestBody = new LockBatchRequestBody();
// 请求加锁的消费者组
requestBody.setConsumerGroup(this.consumerGroup);
// 请求加锁的客户端
requestBody.setClientId(this.mQClientFactory.getClientId());
// 请求加锁的客户端所分配到的mq
requestBody.getMqSet().add(mq);
try {
// 当前消费者实例对新分配到的mq进行加锁,如果成功,返回该mq
Set<MessageQueue> lockedMq =
this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);
for (MessageQueue mmqq : lockedMq) {
ProcessQueue processQueue = this.processQueueTable.get(mmqq);
if (processQueue != null) {
processQueue.setLocked(true);
processQueue.setLastLockTimestamp(System.currentTimeMillis());
}
}
// 加锁成功,lockOK == true
boolean lockOK = lockedMq.contains(mq);
log.info("the message queue lock {}, {} {}",
lockOK ? "OK" : "Failed",
this.consumerGroup,
mq);
return lockOK;
} catch (Exception e) {
log.error("lockBatchMQ exception, " + mq, e);
}
}
return false;
}
如果这个mq加锁成功就会返回true,否则就返回false
消费者本地队列快照ProcessQueue加锁
上面负载均衡的mq加了broker锁之后消费者端就应该开始拉取消息消费了吧,但是由于消费者是直接消费的是快照队列中的消息,所以队列快照也需要根据mq的加锁情况去进行标记是否已经加锁
org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage
if (processQueue.isLocked()) {
if (!pullRequest.isLockedFirst()) {
// 计算出将要从该mq的哪个位置开始消费
final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
boolean brokerBusy = offset < pullRequest.getNextOffset();
log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
pullRequest, offset, brokerBusy);
if (brokerBusy) {
log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
pullRequest, offset);
}
pullRequest.setLockedFirst(true);
pullRequest.setNextOffset(offset);
}
}
// 代码走到这里是什么情况?说明这个mq是刚通过负载均衡新分配到的,并且此时这个新分配的mq在broker端加锁了,而对应的ProcessQueue并没有加锁
// 因为此时消费者实例还没有对此mq对应的ProcessQueue进行加锁(broker端已经加锁),那什么时候这个会对新分配的mq对应的ProcessQueue进行加锁?
// 答案就是顺序消费服务ConsumeMessageOrderlyService每20s会通过负载均衡服务rebalanceImpl去对当前消费者实例分配到的mq进行broker端的加锁(或者续约),而同时也会对mq对应的ProcessQueue进行加锁
// 而在mq对应的ProcessQueue加锁之前,拉取任务都会延迟3s再执行,直到ProcessQueue进行了加锁之后就可以走上面的顺序拉取逻辑分支了
else {
this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
log.info("pull message later because not locked in broker, {}", pullRequest);
return;
}
可以看到在消费者拉取消息之前会去根据ProcessQueue的加锁状态去判断对应的mq有没有加了broker锁,如果加了broker锁就先去获取到mq的消费起始位置,设置好了消费起始位置之后就开始拉取消息了。这里有一个问题,就是ProcessQueue是什么时候开始加锁的?
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#start
public void start() {
if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// 延迟1s执行,每20s对当前消费者实例所分配到的mq进行broker端加锁或者续约锁
ConsumeMessageOrderlyService.this.lockMQPeriodically();
}
}, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
}
}
public synchronized void lockMQPeriodically() {
if (!this.stopped) {
this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll();
}
}
/**
* 对于顺序消费来说,对当前消费者实例所分配到的mq进行broker端加锁
*/
public void lockAll() {
// 对当前消费者实例所分配到的所有mq根据brokerName进行分组
HashMap<String, Set<MessageQueue>> brokerMqs = this.buildProcessQueueTableByBrokerName();
Iterator<Entry<String, Set<MessageQueue>>> it = brokerMqs.entrySet().iterator();
while (it.hasNext()) {
Entry<String, Set<MessageQueue>> entry = it.next();
final String brokerName = entry.getKey();
final Set<MessageQueue> mqs = entry.getValue();
if (mqs.isEmpty())
continue;
FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true);
if (findBrokerResult != null) {
LockBatchRequestBody requestBody = new LockBatchRequestBody();
requestBody.setConsumerGroup(this.consumerGroup);
requestBody.setClientId(this.mQClientFactory.getClientId());
requestBody.setMqSet(mqs);
try {
// 对mq集合进行broker端加锁,返回加锁成功的mq
Set<MessageQueue> lockOKMQSet =
this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);
// 遍历在broker端加锁成功的mq,然后在消费者端也对该mq对应的ProcessQueue进行加锁
for (MessageQueue mq : lockOKMQSet) {
ProcessQueue processQueue = this.processQueueTable.get(mq);
if (processQueue != null) {
if (!processQueue.isLocked()) {
log.info("the message queue locked OK, Group: {} {}", this.consumerGroup, mq);
}
// ProcessQueue标记已加broker锁
processQueue.setLocked(true);
// 更新加broker锁的时间
processQueue.setLastLockTimestamp(System.currentTimeMillis());
}
}
for (MessageQueue mq : mqs) {
// 找到在broker端加锁未成功的mq
if (!lockOKMQSet.contains(mq)) {
ProcessQueue processQueue = this.processQueueTable.get(mq);
if (processQueue != null) {
// 把ProcessQueue设置成未加锁状态
processQueue.setLocked(false);
log.warn("the message queue locked Failed, Group: {} {}", this.consumerGroup, mq);
}
}
}
} catch (Exception e) {
log.error("lockBatchMQ exception, " + mqs, e);
}
}
}
}
在顺序消费服务启动的时候会开始一个定时任务,这个定时任务每隔20s就会把当前消费者所分配到的所有mq向broker申请加锁,broker端返回加锁结果,加锁成功的mq会把对应的ProcessQueue的加锁标记设置为true,同样的加锁不成功的mq对应的ProcessQueue加锁标记就会设置为false
往顺序消费服务ConsumeMessageOrderlyService中提交消费任务
org.apache.rocketmq.client.consumer.PullCallback#onSuccess
// 把从broker端拉取下来并且经过客户端过滤之后的消息放到ProcessQueue中
// 可以根据返回的dispatchToConsume去决定是否需要向消费线程池中提交消费任务,该变量用于保证顺序消费
boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
// 把消费者自定义过滤后的消息交给消费服务(并发消费或者顺序消费)去进行处理,里面包括回调执行我们自己的消费业务逻辑
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
pullResult.getMsgFoundList(),
processQueue,
pullRequest.getMessageQueue(),
dispatchToConsume);
public void submitConsumeRequest(
final List<MessageExt> msgs,
final ProcessQueue processQueue,
final MessageQueue messageQueue,
final boolean dispathToConsume) {
if (dispathToConsume) {
ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
this.consumeExecutor.submit(consumeRequest);
}
}
在消费者从mq中成功拉取到消息之后会存放到对应的队列快照中,并且会返回dispatchToConsume这个变量,这个变量能够控制是否给线程池提交消费任务,因为我们上面也说过了消费者端要保证一个mq的顺序消费的话,必须是一个消费线程对应一个mq才能够保证,那么dispatchToConsume变量什么时候才能为true呢?我们去到processQueue.putMessage方法看看
/**
* 把从broker端拉取下来并且经过客户端过滤之后的消息放到msgTreeMap
* @param msgs 从broker端拉取下来并且经过客户端过滤之后的msg
* @return 该返回值值针对于顺序消费,当dispatchToConsume == true,表示顺序消费服务需要提交一个消费任务到消费线程池,反之不提交
* 而控制dispatchToConsume == true的是当consuming == false的时候,也就是当顺序消费服务把msgTreeMap的消息都消息完了之后才会再次向消费线程池中提交消费任务,这样就可以保证消息的顺序消费
*/
public boolean putMessage(final List<MessageExt> msgs) {
// 是否让顺序消费服务去消费消息
boolean dispatchToConsume = false;
try {
// 加写锁
this.lockTreeMap.writeLock().lockInterruptibly();
try {
int validMsgCnt = 0;
// 遍历拉取到的所有消息
for (MessageExt msg : msgs) {
// 把消息放到msgTreeMap中
MessageExt old = msgTreeMap.put(msg.getQueueOffset(), msg);
if (null == old) {
validMsgCnt++;
// 得到消费队列的最大消息偏移量
this.queueOffsetMax = msg.getQueueOffset();
// 递增记录还未被消费的消息大小
msgSize.addAndGet(msg.getBody().length);
}
}
// 递增记录还未被消费的消息数量
msgCount.addAndGet(validMsgCnt);
// 如果msgTreeMap中有未被消费的消息并且该ProcessQueue还没有被消费者去消费, 那么dispatchToConsume = true, consuming = true
if (!msgTreeMap.isEmpty() && !this.consuming) {
dispatchToConsume = true;
this.consuming = true;
}
if (!msgs.isEmpty()) {
MessageExt messageExt = msgs.get(msgs.size() - 1);
String property = messageExt.getProperty(MessageConst.PROPERTY_MAX_OFFSET);
if (property != null) {
// 计算出还有多少消息未被拉取下来
long accTotal = Long.parseLong(property) - messageExt.getQueueOffset();
if (accTotal > 0) {
this.msgAccCnt = accTotal;
}
}
}
} finally {
this.lockTreeMap.writeLock().unlock();
}
} catch (InterruptedException e) {
log.error("putMessage exception", e);
}
return dispatchToConsume;
}
可以看到当第一次放入消息在队列快照中时,!msgTreeMap.isEmpty() == true,consuming默认等于false,所以dispatchToConsume = true,this.consuming = true,但是当第二次放入消息的时候,由于this.consuming = true,所以dispatchToConsume = false,也就是不会给线程池添加消费任务,那么consuming这个变量什么时候会变成false呢?我们现在去看takeMessage方法
/**
* 该方法只针对顺序消费服务去使用
* 从msgTreeMap中获取msg进行消费,如果msgTreeMap中的msg被消费完了,那么会把consuming属性设置为false
* @param batchSize 从msgTreeMap中获取多少个msg去消费
* @return 返回获取到的msg集合
*/
public List<MessageExt> takeMessages(final int batchSize) {
List<MessageExt> result = new ArrayList<MessageExt>(batchSize);
final long now = System.currentTimeMillis();
try {
// 加写锁
this.lockTreeMap.writeLock().lockInterruptibly();
this.lastConsumeTimestamp = now;
try {
if (!this.msgTreeMap.isEmpty()) {
for (int i = 0; i < batchSize; i++) {
// 获取到最小偏移量的消息,并在msgTreeMap中移除
Map.Entry<Long, MessageExt> entry = this.msgTreeMap.pollFirstEntry();
if (entry != null) {
result.add(entry.getValue());
consumingMsgOrderlyTreeMap.put(entry.getKey(), entry.getValue());
} else {
break;
}
}
}
if (result.isEmpty()) {
consuming = false;
}
} finally {
this.lockTreeMap.writeLock().unlock();
}
} catch (InterruptedException e) {
log.error("take Messages exception", e);
}
return result;
}
首先先说明一下takeMessage方法是在消费消息的时候被调用的,其中我们可以看到每次消费的时候都会从msgTreeMap中去获取到消息并从其中移除掉,当msgTreeMap中的消息都被消费完了的时候,此时consuming就重置为false,那么当下一次调用putMessage方法的时候返回的dispatchToConsume变量值就等于true了。总结地也就是说,为了保证顺序消费,消费者每拉取到一批数据放入到队列快照中的时候都会检查一下队列快照中是否还存在未消费的消息,如果有则不会往线程池中添加消费任务,反之则添加
执行顺序消费任务
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService.ConsumeRequest#run
if (this.processQueue.isDropped()) {
log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
return;
}
// 获取到mq对应的锁对象
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
// 先加锁,避免有多个消费任务同时执行,保证消费消息的顺序性
synchronized (objLock) {
if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
|| (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
final long beginTime = System.currentTimeMillis();
for (boolean continueConsume = true; continueConsume; ) {
// 如果该mq已经被dropped,那么停止对该mq的消费
if (this.processQueue.isDropped()) {
log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
break;
}
// 条件成立:集群模式并且队列没有被加锁
if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
&& !this.processQueue.isLocked()) {
log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
// 尝试对队列进行重新加锁,并且重新往线程池中添加消费任务
ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
// 消费线程结束
break;
}
// 条件成立:集群模式并且锁已过期
if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
&& this.processQueue.isLockExpired()) {
log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);
// 尝试对队列进行重新加锁,并且重新往线程池中添加消费任务
ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
// 消费线程结束
break;
}
// 如果该消费线程已经持续消费工作超过了1分钟,那么就结束当前消费线程重新提交一个消费任务
long interval = System.currentTimeMillis() - beginTime;
if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {
ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);
// 消费线程结束
break;
}
final int consumeBatchSize =
ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
// 从ProcessQueue中获取指定数量的msg
List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);
defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());
if (!msgs.isEmpty()) {
final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);
ConsumeOrderlyStatus status = null;
ConsumeMessageContext consumeMessageContext = null;
// 执行消息消费的前置钩子方法
if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
consumeMessageContext = new ConsumeMessageContext();
consumeMessageContext
.setConsumerGroup(ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumerGroup());
consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
consumeMessageContext.setMq(messageQueue);
consumeMessageContext.setMsgList(msgs);
consumeMessageContext.setSuccess(false);
// init the consume context type
consumeMessageContext.setProps(new HashMap<String, String>());
ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
}
long beginTimestamp = System.currentTimeMillis();
ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
boolean hasException = false;
try {
this.processQueue.getLockConsume().lock();
if (this.processQueue.isDropped()) {
log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
this.messageQueue);
break;
}
// 执行用户自定义的顺序消费回调
status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
} catch (Throwable e) {
log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}",
RemotingHelper.exceptionSimpleDesc(e),
ConsumeMessageOrderlyService.this.consumerGroup,
msgs,
messageQueue);
hasException = true;
} finally {
this.processQueue.getLockConsume().unlock();
}
if (null == status
|| ConsumeOrderlyStatus.ROLLBACK == status
|| ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {
log.warn("consumeMessage Orderly return not OK, Group: {} Msgs: {} MQ: {}",
ConsumeMessageOrderlyService.this.consumerGroup,
msgs,
messageQueue);
}
long consumeRT = System.currentTimeMillis() - beginTimestamp;
// 根据消费回调返回值去得到returnType
if (null == status) {
if (hasException) {
returnType = ConsumeReturnType.EXCEPTION;
} else {
returnType = ConsumeReturnType.RETURNNULL;
}
} else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) {
returnType = ConsumeReturnType.TIME_OUT;
} else if (ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT == status) {
returnType = ConsumeReturnType.FAILED;
} else if (ConsumeOrderlyStatus.SUCCESS == status) {
returnType = ConsumeReturnType.SUCCESS;
}
if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
consumeMessageContext.getProps().put(MixAll.CONSUME_CONTEXT_TYPE, returnType.name());
}
if (null == status) {
status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
// 执行消息消费的后置钩子方法
if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) {
consumeMessageContext.setStatus(status.toString());
consumeMessageContext
.setSuccess(ConsumeOrderlyStatus.SUCCESS == status || ConsumeOrderlyStatus.COMMIT == status);
ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
}
ConsumeMessageOrderlyService.this.getConsumerStatsManager()
.incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);
// 根据消费结果执行不同的逻辑
continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
} else {
continueConsume = false;
}
}
} else {
if (this.processQueue.isDropped()) {
log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
return;
}
ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
}
}
}
可以看到一开始会从messageQueueLock中根据mq去获取到对应的锁对象,这也进一步保证了一个mq被顺序消费。
接着会有下面几步的处理:
- 如果该mq已经被dropped,结束消费线程
- 如果是集群模式并且队列没有加锁,会尝试对队列重新加锁,延迟提交一个新的消费任务,结束当前消费线程
- 如果是集群模式并且队列的锁已经过期了,会尝试对队列重新加锁,延迟提交一个新的消费任务,结束当前消费线程
- 如果该消费线程已经持续消费工作超过了1分钟,那么就结束当前消费线程重新提交一个消费任务
- 从ProcessQueue中获取指定数量的消息
- 执行消息消费的前置钩子方法
- 执行用户自定义的监听回调方法
- 执行消息消费的后置钩子方法
- 根据监听回调方法返回值去执行不同的处理
其中第9步就是根据我们在监听回调方法的返回值去做出不同的处理,而对于顺序消费来说,我们一般都返回两个值,分别是ConsumeOrderlyStatus.SUCCESS以及ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT,下面我们来看下消费者在收到用户返回的这两种类型的返回值的时候会分别怎样去处理
public boolean processConsumeResult(
final List<MessageExt> msgs,
final ConsumeOrderlyStatus status,
final ConsumeOrderlyContext context,
final ConsumeRequest consumeRequest
) {
// 控制外层的消费任务是否继续执行
boolean continueConsume = true;
// 已消费消息的最大偏移量
long commitOffset = -1L;
// 自动提交
if (context.isAutoCommit()) {
switch (status) {
case COMMIT:
case ROLLBACK:
log.warn("the message queue consume result is illegal, we think you want to ack these message {}",
consumeRequest.getMessageQueue());
// 消息消费成功
case SUCCESS:
// 返回已消费的消息中最大的偏移量
commitOffset = consumeRequest.getProcessQueue().commit();
this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
break;
// 需要挂起
case SUSPEND_CURRENT_QUEUE_A_MOMENT:
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
if (checkReconsumeTimes(msgs)) {
// 把消费失败的消息重新放入到ProcessQueue中
consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs);
// 延迟(默认1s)提交消费任务,保证消费失败的消息能够被重新消费
this.submitConsumeRequestLater(
consumeRequest.getProcessQueue(),
consumeRequest.getMessageQueue(),
context.getSuspendCurrentQueueTimeMillis());
// 本次消费任务结束
continueConsume = false;
}
// 消息已经超过最大重试次数了,直接提交,忽略该消息(当成消费成功)
else {
commitOffset = consumeRequest.getProcessQueue().commit();
}
break;
default:
break;
}
}
// 手动提交
else {
switch (status) {
case SUCCESS:
this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
break;
case COMMIT:
commitOffset = consumeRequest.getProcessQueue().commit();
break;
case ROLLBACK:
consumeRequest.getProcessQueue().rollback();
this.submitConsumeRequestLater(
consumeRequest.getProcessQueue(),
consumeRequest.getMessageQueue(),
context.getSuspendCurrentQueueTimeMillis());
continueConsume = false;
break;
case SUSPEND_CURRENT_QUEUE_A_MOMENT:
this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
if (checkReconsumeTimes(msgs)) {
consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs);
this.submitConsumeRequestLater(
consumeRequest.getProcessQueue(),
consumeRequest.getMessageQueue(),
context.getSuspendCurrentQueueTimeMillis());
continueConsume = false;
}
break;
default:
break;
}
}
if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
// 向broker提交最大的已消费偏移量
this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);
}
return continueConsume;
}
我们只看自动提交就可以了,如果监听回调中返回的是SUCCESS,那么就调用commit方法返回已消费的消息中的最大偏移量,然后再向broker提交这个最大已消费偏移量;如果监听回调返回的是SUSPEND_CURRENT_QUEUE_A_MOMENT,那么此时就需要判断下该消息是否已超过重试次数,如果没有超过,把该消息重新放回到ProcessQueue中然后再次提交一个消费任务,然后就会结束当前的消费任务,等待新提交的消费任务去对消费失败的消息进行重试,这也是顺序消费与并发消费在处理消费失败时不同的一点,顺序消费为了保证消费是顺序被消费的,所以就不能像并发消费那样对消息进行回退重试。但是如果顺序消费时该消息重试次数满了呢?这种情况消费者会忽略该消息,也就是把该消息当做是成功消费了,然后再直接向broker提交最大消费偏移量