RocketMQ如何在消费失败的时候保证消费顺序呢?

2,111 阅读3分钟

我正在参与掘金创作者训练营第5期,点击了解活动详情

这个需要从消费任务实现类ConsumerRequest和本地缓存队列ProcessQueue的设计来看主要差异。

并发消息的消费请求

class ConsumeRequest implements Runnable {
    private final List<MessageExt> msgs;
    private final ProcessQueue processQueue;
    private final MessageQueue messageQueue;

    public ConsumeRequest(List<MessageExt> msgs, ProcessQueue processQueue, MessageQueue messageQueue) {
        this.msgs = msgs;
        this.processQueue = processQueue;
        this.messageQueue = messageQueue;
    }

    public List<MessageExt> getMsgs() {
        return msgs;
    }

    public ProcessQueue getProcessQueue() {
        return processQueue;
    }

    @Override
    public void run() {
    ...
    }

顺序消息的消费请求

class ConsumeRequest implements Runnable {
    private final ProcessQueue processQueue;
    private final MessageQueue messageQueue;

    public ConsumeRequest(ProcessQueue processQueue, MessageQueue messageQueue) {
        this.processQueue = processQueue;
        this.messageQueue = messageQueue;
    }

    public ProcessQueue getProcessQueue() {
        return processQueue;
    }

    public MessageQueue getMessageQueue() {
        return messageQueue;
    }

    @Override
    public void run() {
    ...
    }

从以上代码可以看出顺序消息的ConsumeRequest中并没有保存需要消费的消息。它会通过

List<MessageExt> msgs = this.processQueue.takeMessages(consumeBatchSize);

获取需要消费的消息并同步消费。

这里我们看看ProcessQueue对于顺序消费有什么特殊的支持。

public class ProcessQueue {
    ...
    private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
    ...
    private final TreeMap<Long, MessageExt> consumingMsgOrderlyTreeMap = new TreeMap<Long, MessageExt>();
    ...
}

在他的属性里边有两个较为重要的属性

  • msgTreeMap的key是消息的物理位点值,value是消息对象,这个容器是ProcessQueue用来缓存本地消息的,他会按照key值按顺序排列。
  • consumingMsgOrderlyTreeMap的key也是消息物理位点值,value是消息对象,保存的是当前正在处理的消息集合。

再下来看看takeMessages方法:

public List<MessageExt> takeMessages(final int batchSize) {
    List<MessageExt> result = new ArrayList<MessageExt>(batchSize);
    final long now = System.currentTimeMillis();
    try {
        this.treeMapLock.writeLock().lockInterruptibly();
        this.lastConsumeTimestamp = now;
        try {
            if (!this.msgTreeMap.isEmpty()) {
                for (int i = 0; i < batchSize; i++) {
                    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.treeMapLock.writeLock().unlock();
        }
    } catch (InterruptedException e) {
        log.error("take Messages exception", e);
    }

    return result;
}

这段代码从msgTreeMap中取batchSize个消息放入consumingMsgOrderlyTreeMap给客户消费,由于被 this.treeMapLock.writeLock().lockInterruptibly();锁定,并且获取的消息是按照物理位点按顺序排列的,所以消费时只能按照物理位点顺序消费

可是重点来了,若是消费失败了,又如何保证消费顺序呢?

当处理消费结果的时候有一定的支持,我们一起看看这个方法。

continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);

这里的三个参数意义是:

  • msgs 当前处理的一批消息
  • ststus消费结果的状态
  • context和this对于顺序消费作用不大

这个方法里分为自动提交offset手动提交offset,以自动提交为例说明:

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:
        //成功后执行commit提交当前消费位点
        commitOffset = consumeRequest.getProcessQueue().commit();
        this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
        break;
    case SUSPEND_CURRENT_QUEUE_A_MOMENT:
        //统计失败的TPS
        this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
        if (checkReconsumeTimes(msgs)) {
            //从consumingMsgOrderlyTreeMap中删除当前消息,放回msgTreeMap中
            consumeRequest.getProcessQueue().makeMessageToConsumeAgain(msgs);
            this.submitConsumeRequestLater(
                consumeRequest.getProcessQueue(),
                consumeRequest.getMessageQueue(),
                context.getSuspendCurrentQueueTimeMillis());
            continueConsume = false;
        } else {
            commitOffset = consumeRequest.getProcessQueue().commit();
        }
        break;
    default:
        break;
}
  • 对于成功消费的消息,直接提交当前消费位点
  • 对于消费失败的消息,则从consumingMsgOrderlyTreeMap中删除当前消息,并放回msgTreeMap中重新消费

放回msgTreeMap中之后,submitConsumeRequestLater方法会执行一个定时任务,延迟一定时间后重新将消息消费请求发送到消费线程池中,供下一轮消费。

private void submitConsumeRequestLater(
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final long suspendTimeMillis
) {
    long timeMillis = suspendTimeMillis;
    if (timeMillis == -1) {
        timeMillis = this.defaultMQPushConsumer.getSuspendCurrentQueueTimeMillis();
    }

    if (timeMillis < 10) {
        timeMillis = 10;
    } else if (timeMillis > 30000) {
        timeMillis = 30000;
    }

    this.scheduledExecutorService.schedule(new Runnable() {

        @Override
        public void run() {
            ConsumeMessageOrderlyService.this.submitConsumeRequest(null, processQueue, messageQueue, true);
        }
    }, timeMillis, TimeUnit.MILLISECONDS);
}

也就是说,当顺序消息消费失败的时候,这个失败的消息又会被放入待消费的队列,当继续从待消费队列获取消息的时候,还是会取到这个失败的消息进行消费,以此来保证消费消息的顺序性。