RocketMQ学习(八)——RocketMQ顺序消息

顺序消息

顺序消息(FIFO 消息)是 MQ 提供的一种严格按照顺序进行发布和消费的消息类型。RocketMQ可以严格的保证消息有序。但这个顺序,不是全局顺序,只是分区(queue)顺序。要全局顺序只能一个分区。需要从发送到消费整个过程中保证有序,所以顺序消息具体表现为

  • 发送消息是顺序的:需要同一线程发送一组消息,而调用的produce发送消息的方法也要是同步的。
  • broker存储消息是顺序的
  • consumer消费是顺序的:需要保证一个queue只在一个线程内被消费。

发送顺序消息

因为broker存储消息有序的前提是producer发送消息是有序的,所以这两个结合在一起说。消息发布是有序的含义:producer发送消息应该是依次发送的,所以要求发送消息的时候保证:

  • 需要同一线程发送一组消息,而消息不能异步发送,同步发送的时候才能保证broker收到是有序的。
  • 每次发送选择的是同一个MessageQueue。
Producer 顺序发送

下面用订单进行示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。

/**
 * Producer,发送顺序消息
 */
public class Producer {
	
    public static void main(String[] args) throws IOException {
        try {
            DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
 
            producer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
 
            producer.start();
 
            String[] tags = new String[] { "TagA", "TagC", "TagD" };
            
            // 订单列表
            List<OrderDemo> orderList =  new Producer().buildOrders();
            
            Date date = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateStr = sdf.format(date);
            for (int i = 0; i < 10; i++) {
                // 加个时间后缀
                String body = dateStr + " Hello RocketMQ " + orderList.get(i);
                Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i, body.getBytes());
 
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                        Long id = (Long) arg;
                        long index = id % mqs.size();
                        return mqs.get((int)index);
                    }
                }, orderList.get(i).getOrderId());//订单id
 
                System.out.println(sendResult + ", body:" + body);
            }
            
            producer.shutdown();
 
        } catch (MQClientException e) {
            e.printStackTrace();
        } catch (RemotingException e) {
            e.printStackTrace();
        } catch (MQBrokerException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.in.read();
    }
    
    /**
     * 生成模拟订单数据 
     */
    private List<OrderDemo> buildOrders() {
    	List<OrderDemo> orderList = new ArrayList<OrderDemo>();
 
    	OrderDemo orderDemo = new OrderDemo();
        orderDemo.setOrderId(15103111039L);
    	orderDemo.setDesc("创建");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103111065L);
    	orderDemo.setDesc("创建");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103111039L);
    	orderDemo.setDesc("付款");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103117235L);
    	orderDemo.setDesc("创建");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103111065L);
    	orderDemo.setDesc("付款");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103117235L);
    	orderDemo.setDesc("付款");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103111065L);
    	orderDemo.setDesc("完成");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103111039L);
    	orderDemo.setDesc("推送");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103117235L);
    	orderDemo.setDesc("完成");
    	orderList.add(orderDemo);
    	
    	orderDemo = new OrderDemo();
    	orderDemo.setOrderId(15103111039L);
    	orderDemo.setDesc("完成");
    	orderList.add(orderDemo);
    	
    	return orderList;
    }

输出:
在这里插入图片描述
从图中红色框可以看出,orderId等于15103111039的订单被顺序放入queueId等于7的队列。queueOffset同时在顺序增长。

源码

从刚开始的例子中发送消息的时候,会调用下面的方法:

public SendResult send(Message msg, MessageQueueSelector selector, Object arg, long timeout)
    throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    return this.sendSelectImpl(msg, selector, arg, CommunicationMode.SYNC, null, timeout);
}

CommunicationMode.SYNC表明了producer发送消息的时候是同步发送的。同步发送表示,producer发送消息之后不会立即返回,会等待broker的response。broker收到producer的请求之后虽然是启动线程处理的,但是在线程中将消息写入commitLog中以后会发送response给producer,producer在收到broker的response并且是处理成功之后才算是消息发送成功。

private SendResult sendSelectImpl(
    Message msg,
    MessageQueueSelector selector,
    Object arg,
    final CommunicationMode communicationMode,
    final SendCallback sendCallback, final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    this.makeSureStateOK();
    Validators.checkMessage(msg, this.defaultMQProducer);

    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        MessageQueue mq = null;
        try {
            // 调用用户指定的select方法来选出一个queue,如果是全局顺序,用户必须保证自己选出的queue是同一个
            mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
        } catch (Throwable e) {
            throw new MQClientException("select message queue throwed exception.", e);
        }

        if (mq != null) {
            return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, null, timeout);
        } else {
            throw new MQClientException("select message queue return null.", null);
        }
    }

    throw new MQClientException("No route info for this topic, " + msg.getTopic(), null);
}

这样每次发送的消息都是同一个MessageQueue,也就是都会发送到同一个broker,这个发送消息的过程都保证了顺序,也就保证了broker存储在CommitLog中的消息也是顺序的。

顺序消费

/**
 * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
 */
public class ConsumerInOrder {
 
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
* 如果非第一次启动,那么按照上次消费的位置继续消费 */
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); consumer.subscribe("TopicTestjjj", "TagA || TagC || TagD"); consumer.registerMessageListener(new MessageListenerOrderly() { Random random = new Random(); @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { context.setAutoCommit(true); System.out.print(Thread.currentThread().getName() + " Receive New Messages: " ); for (MessageExt msg: msgs) { System.out.println(msg + ", content:" + new String(msg.getBody())); } try { //模拟业务逻辑处理中... TimeUnit.SECONDS.sleep(random.nextInt(10)); } catch (Exception e) { e.printStackTrace(); } return ConsumeOrderlyStatus.SUCCESS; } }); consumer.start(); System.out.println("Consumer Started."); } }

输出:
在这里插入图片描述
MessageListenerOrderly能够保证顺序消费,从图中我们也看到了期望的结果。图中的输出是只启动了一个消费者时的输出,看起来订单号还是混在一起,但是每组订单号之间是有序的。因为消息发送时被分配到了三个队列(参见前面生产者输出日志),那么这三个队列的消息被这唯一消费者消费。

源码

还是开始的例子中,顺序消费和普通消费的listener是不一样的,顺序消费需要实现的是下面这个接口。

org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly

在consumer启动的时候会根据listener的类型判断应该使用哪一个service来消费。

// org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
    // 顺序消息
    this.consumeOrderly = true;
    this.consumeMessageService =
        new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
    // 非顺序消息
    this.consumeOrderly = false;
    this.consumeMessageService =
        new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}

集群模式下,Consumer 更新属于自己的消息队列时,会向 Broker 锁定该消息队列(广播模式下不需要)。如果锁定失败,则更新失败,即该消息队列不属于自己,不能进行消费。核心代码如下:

 1: //【RebalanceImpl.java】
 2: private boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet, final boolean isOrder) {
 3: // ..... 此处省略部分代码 
 4:     // 增加 不在processQueueTable && 存在于mqSet 里的消息队列。
 5:     List<PullRequest> pullRequestList = new ArrayList<>(); // 拉消息请求数组
 6:     for (MessageQueue mq : mqSet) {
 7:         if (!this.processQueueTable.containsKey(mq)) {
 8:             if (isOrder && !this.lock(mq)) { // 顺序消息锁定消息队列
 9:                 log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
10:                 continue;
11:             }
12: 
13:             this.removeDirtyOffset(mq);
14:             ProcessQueue pq = new ProcessQueue();
15:             long nextOffset = this.computePullFromWhere(mq);
16:             if (nextOffset >= 0) {
17:                 ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
18:                 if (pre != null) {
19:                     log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
20:                 } else {
21:                     log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
22:                     PullRequest pullRequest = new PullRequest();
23:                     pullRequest.setConsumerGroup(consumerGroup);
24:                     pullRequest.setNextOffset(nextOffset);
25:                     pullRequest.setMessageQueue(mq);
26:                     pullRequest.setProcessQueue(pq);
27:                     pullRequestList.add(pullRequest);
28:                     changed = true;
29:                 }
30:             } else {
31:                 log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
32:             }
33:         }
34:     }
35: 
36: // ..... 此处省略部分代码 
37: }
38: 
39: // ⬇️⬇️⬇️【RebalanceImpl.java】
40: /**
41:  * 请求Broker获得指定消息队列的分布式锁
42:  *
43:  * @param mq 队列
44:  * @return 是否成功
45:  */
46: public boolean lock(final MessageQueue mq) {
47:     FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true);
48:     if (findBrokerResult != null) {
49:         LockBatchRequestBody requestBody = new LockBatchRequestBody();
50:         requestBody.setConsumerGroup(this.consumerGroup);
51:         requestBody.setClientId(this.mQClientFactory.getClientId());
52:         requestBody.getMqSet().add(mq);
53: 
54:         try {
55:             // 请求Broker获得指定消息队列的分布式锁
56:             Set<MessageQueue> lockedMq =
57:                 this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);
58: 
59:             // 设置消息处理队列锁定成功。锁定消息队列成功,可能本地没有消息处理队列,设置锁定成功会在lockAll()方法。
60:             for (MessageQueue mmqq : lockedMq) {
61:                 ProcessQueue processQueue = this.processQueueTable.get(mmqq);
62:                 if (processQueue != null) {
63:                     processQueue.setLocked(true);
64:                     processQueue.setLastLockTimestamp(System.currentTimeMillis());
65:                 }
66:             }
67: 
68:             boolean lockOK = lockedMq.contains(mq);
69:             log.info("the message queue lock {}, {} {}",
70:                 lockOK ? "OK" : "Failed",
71:                 this.consumerGroup,
72:                 mq);
73:             return lockOK;
74:         } catch (Exception e) {
75:             log.error("lockBatchMQ exception, " + mq, e);
76:         }
77:     }
78: 
79:     return false;
80: }

这是一个分布式锁,保证了一个queue只会被一个消费者锁定和消费。如果锁定成功,则添加到拉取任务中,如果锁定未成功,说明虽然发送了消息队列重新负载,但该消息队列还未被释放,本次负载周期不会进行消息拉取。
Broker 消息队列锁会过期,默认配置 30s。因此,Consumer 需要不断向 Broker 刷新该锁过期时间,默认配置 20s 刷新一次。核心代码如下:

1: // ⬇️⬇️⬇️【ConsumeMessageOrderlyService.java】
 2: public void start() {
 3:     if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
 4:         this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
 5:             @Override
 6:             public void run() {
 7:                 ConsumeMessageOrderlyService.this.lockMQPeriodically();
 8:             }
 9:         }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
10:     }
11: }

集群模式下,Consumer 移除自己的消息队列时,会向 Broker 解锁该消息队列(广播模式下不需要)。核心代码如下:

 1: // ⬇️⬇️⬇️【RebalancePushImpl.java】
 2: /**
 3:  * 移除不需要的队列相关的信息
 4:  * 1. 持久化消费进度,并移除之
 5:  * 2. 顺序消费&集群模式,解锁对该队列的锁定
 6:  *
 7:  * @param mq 消息队列
 8:  * @param pq 消息处理队列
 9:  * @return 是否移除成功
10:  */
11: @Override
12: public boolean removeUnnecessaryMessageQueue(MessageQueue mq, ProcessQueue pq) {
13:     // 同步队列的消费进度,并移除之。
14:     this.defaultMQPushConsumerImpl.getOffsetStore().persist(mq);
15:     this.defaultMQPushConsumerImpl.getOffsetStore().removeOffset(mq);
16:     // 集群模式下,顺序消费移除时,解锁对队列的锁定
17:     if (this.defaultMQPushConsumerImpl.isConsumeOrderly()
18:         && MessageModel.CLUSTERING.equals(this.defaultMQPushConsumerImpl.messageModel())) {
19:         try {
20:             if (pq.getLockConsume().tryLock(1000, TimeUnit.MILLISECONDS)) {
21:                 try {
22:                     return this.unlockDelay(mq, pq);
23:                 } finally {
24:                     pq.getLockConsume().unlock();
25:                 }
26:             } else {
27:                 log.warn("[WRONG]mq is consuming, so can not unlock it, {}. maybe hanged for a while, {}", //
28:                     mq, //
29:                     pq.getTryUnlockTimes());
30: 
31:                 pq.incTryUnlockTimes();
32:             }
33:         } catch (Exception e) {
34:             log.error("removeUnnecessaryMessageQueue Exception", e);
35:         }
36: 
37:         return false;
38:     }
39:     return true;
40: }
41: 
42: // ⬇️⬇️⬇️【RebalancePushImpl.java】
43: /**
44:  * 延迟解锁 Broker 消息队列锁
45:  * 当消息处理队列不存在消息,则直接解锁
46:  *
47:  * @param mq 消息队列
48:  * @param pq 消息处理队列
49:  * @return 是否解锁成功
50:  */
51: private boolean unlockDelay(final MessageQueue mq, final ProcessQueue pq) {
52:     if (pq.hasTempMessage()) { // TODO 疑问:为什么要延迟移除
53:         log.info("[{}]unlockDelay, begin {} ", mq.hashCode(), mq);
54:         this.defaultMQPushConsumerImpl.getmQClientFactory().getScheduledExecutorService().schedule(new Runnable() {
55:             @Override
56:             public void run() {
57:                 log.info("[{}]unlockDelay, execute at once {}", mq.hashCode(), mq);
58:                 RebalancePushImpl.this.unlock(mq, true);
59:             }
60:         }, UNLOCK_DELAY_TIME_MILLS, TimeUnit.MILLISECONDS);
61:     } else {
62:         this.unlock(mq, true);
63:     }
64:     return true;
65: }

第 20 至 32 行 :获取消息队列消费锁,避免和消息队列消费冲突。如果获取锁失败,则移除消息队列失败,等待下次重新分配消费队列时,再进行移除。如果未获得锁而进行移除,则可能出现另外的 Consumer 和当前 Consumer 同时消费该消息队列,导致消息无法严格顺序消费。

第 51 至 64 行 :解锁 Broker 消息队列锁。如果消息处理队列存在剩余消息,则延迟解锁 Broker 消息队列锁。

consumer拉取消息是按照offset拉取的,所以consumer能保证拉取到consumer的消息是连续有序的,但是consumer拉取到消息后又启动了线程去处理消息,所以就不能保证先拉取到的消息先处理

public void pullMessage(final PullRequest pullRequest) {
	...
	PullCallback pullCallback = new PullCallback() {
		...
		public void onSuccess(PullResult pullResult) {
			...
			boolean dispathToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
			DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(pullResult.getMsgFoundList(),processQueue,pullRequest.getMessageQueue(),dispathToConsume);
			...
		}
	}	
}

// org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#submitConsumeRequest
public void submitConsumeRequest(
    final List<MessageExt> msgs,
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final boolean dispathToConsume) {
    if (dispathToConsume) {
        // 拉取到的消息构造ConsumeRequest,然后放入线程池等待执行
        ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
        this.consumeExecutor.submit(consumeRequest);
    }
}

上面的方法是向broker拉取消息并消费的方法,首先获取的消息后会调用。

processQueue.putMessage(pullResult.getMsgFoundList());

先说一下ProcessQueue这个关键的数据结构。一个MessageQueue对应一个ProcessQueue,是一个有序队列,该队列记录一个queueId下所有从brokerpull回来的消息,如果消费成功了就会从队列中删除。ProcessQueue有序的原因是维护了一个TreeMap。

private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();

msgTreeMap:里面维护了从broker pull回来的所有消息,TreeMap是有序的,key是Long类型的,没有指定comparator,默认是将key强转为Comparable,然后进行比较,因为key是当前消息的offset,而Long实现了Comparable接口,所以msgTreeMap里面的消息是按照offset排序的。

所以是ProcessQueue保证了拉取回来的消息是有序的,继续上面说到的启动线程执行ConsumeRequest.run方法来消费消息。

  1: // ⬇️⬇️⬇️【ConsumeMessageOrderlyService.java】
  2: class ConsumeRequest implements Runnable {
  3: 
  4:     /**
  5:      * 消息处理队列
  6:      */
  7:     private final ProcessQueue processQueue;
  8:     /**
  9:      * 消息队列
 10:      */
 11:     private final MessageQueue messageQueue;
 12: 
 13:     @Override
 14:     public void run() {
 15:         if (this.processQueue.isDropped()) {
 16:             log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
 17:             return;
 18:         }
 19: 
 20:         // 获得 Consumer 消息队列锁,保证一个队列只有一个线程访问,因为顺序消息只有一个队列,也就保证了只有一个线程消费
 21:         final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
 22:         synchronized (objLock) {
 23:             // (广播模式) 或者 (集群模式 && Broker消息队列锁有效)
 24:             if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
 25:                 || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
 26:                 final long beginTime = System.currentTimeMillis();
 27:                 // 循环
 28:                 for (boolean continueConsume = true; continueConsume; ) {
 29:                     if (this.processQueue.isDropped()) {
 30:                         log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
 31:                         break;
 32:                     }
 33: 
 34:                     // 消息队列分布式锁未锁定,提交延迟获得锁并消费请求
 35:                     if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
 36:                         && !this.processQueue.isLocked()) {
 37:                         log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
 38:                         ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
 39:                         break;
 40:                     }
 41:                     // 消息队列分布式锁已经过期,提交延迟获得锁并消费请求
 42:                     if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
 43:                         && this.processQueue.isLockExpired()) {
 44:                         log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);
 45:                         ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
 46:                         break;
 47:                     }
 48: 
 49:                     // 当前周期消费时间超过连续时长,默认:60s,提交延迟消费请求。默认情况下,每消费1分钟休息10ms。
 50:                     long interval = System.currentTimeMillis() - beginTime;
 51:                     if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {
 52:                         ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);
 53:                         break;
 54:                     }
 55: 
 56:                     // 获取消费消息。此处和并发消息请求不同,并发消息请求已经带了消费哪些消息。
 57:                     final int consumeBatchSize = ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
 58:                     List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);
 59:                     if (!msgs.isEmpty()) {
 60:                         final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);
 61: 
 62:                         ConsumeOrderlyStatus status = null;
 63: 
 64:                         // ....省略代码:Hook:before
 65: 
 66:                         // 执行消费
 67:                         long beginTimestamp = System.currentTimeMillis();
 68:                         ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
 69:                         boolean hasException = false;
 70:                         try {
 71:                             this.processQueue.getLockConsume().lock(); // 锁定队列消费锁
 72: 
 73:                             if (this.processQueue.isDropped()) {
 74:                                 log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
 75:                                     this.messageQueue);
 76:                                 break;
 77:                             }
 78: 
 79:                             status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
 80:                         } catch (Throwable e) {
 81:                             log.warn("consumeMessage exception: {} Group: {} Msgs: {} MQ: {}", //
 82:                                 RemotingHelper.exceptionSimpleDesc(e), //
 83:                                 ConsumeMessageOrderlyService.this.consumerGroup, //
 84:                                 msgs, //
 85:                                 messageQueue);
 86:                             hasException = true;
 87:                         } finally {
 88:                             this.processQueue.getLockConsume().unlock(); // 锁定队列消费锁
 89:                         }
 90: 
 91:                         // ....省略代码:解析消费结果状态
 92: 
 93:                         // ....省略代码:Hook:after
 94: 
 95:                         ConsumeMessageOrderlyService.this.getConsumerStatsManager()
 96:                             .incConsumeRT(ConsumeMessageOrderlyService.this.consumerGroup, messageQueue.getTopic(), consumeRT);
 97: 
 98:                         // 处理消费结果
 99:                         continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
100:                     } else {
101:                         continueConsume = false;
102:                     }
103:                 }
104:             } else {
105:                 if (this.processQueue.isDropped()) {
106:                     log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
107:                     return;
108:                 }
109: 
110:                 ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
111:             }
112:         }
113:     }
114: 
115: }

加锁保证只有一个线程可以消费,从ProcessQueue中获取出来的消息是有序的,consumer保证了消费的有序性。

处理消费结果

顺序消费消息结果 (ConsumeOrderlyStatus) 有四种情况:

  • SUCCESS :消费成功但不提交。
  • ROLLBACK :消费失败,消费回滚。
  • COMMIT :消费成功提交并且提交。
  • SUSPEND_CURRENT_QUEUE_A_MOMENT :消费失败,挂起消费队列一会会,稍后继续消费。
    考虑到 ROLLBACK 、COMMIT 暂时只使用在 MySQL binlog 场景,官方将这两状态标记为 @Deprecated。当然,相应的实现逻辑依然保留。

在并发消费场景时,如果消费失败,Consumer 会将消费失败消息发回到 Broker 重试队列,跳过当前消息,等待下次拉取该消息再进行消费。

但是在完全严格顺序消费消费时,这样做显然不行。也因此,消费失败的消息,会挂起队列一会会,稍后继续消费。不过消费失败的消息一直失败,也不可能一直消费。当超过消费重试上限时,Consumer 会将消费失败超过上限的消息发回到 Broker 死信队列。

让我们来看看代码:

  1: // ⬇️⬇️⬇️【ConsumeMessageOrderlyService.java】
  2: /**
  3:  * 处理消费结果,并返回是否继续消费
  4:  *
  5:  * @param msgs 消息
  6:  * @param status 消费结果状态
  7:  * @param context 消费Context
  8:  * @param consumeRequest 消费请求
  9:  * @return 是否继续消费
 10:  */
 11: public boolean processConsumeResult(//
 12:     final List<MessageExt> msgs, //
 13:     final ConsumeOrderlyStatus status, //
 14:     final ConsumeOrderlyContext context, //
 15:     final ConsumeRequest consumeRequest//
 16: ) {
 17:     boolean continueConsume = true;
 18:     long commitOffset = -1L;
 19:     if (context.isAutoCommit()) {
 20:         switch (status) {
 21:             case COMMIT:
 22:             case ROLLBACK:
 23:                 log.warn("the message queue consume result is illegal, we think you want to ack these message {}", consumeRequest.getMessageQueue());
 24:             case SUCCESS:
 25:                 // 提交消息已消费成功到消息处理队列
 26:                 commitOffset = consumeRequest.getProcessQueue().commit();
 27:                 // 统计
 28:                 this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
 29:                 break;
 30:             case SUSPEND_CURRENT_QUEUE_A_MOMENT:
 31:                 // 统计
 32:                 this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
 33:                 if (checkReconsumeTimes(msgs)) { // 计算是否暂时挂起(暂停)消费N毫秒,默认:10ms
 34:                     // 设置消息重新消费
 35:                     consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs);
 36:                     // 提交延迟消费请求
 37:                     this.submitConsumeRequestLater(//
 38:                         consumeRequest.getProcessQueue(), //
 39:                         consumeRequest.getMessageQueue(), //
 40:                         context.getSuspendCurrentQueueTimeMillis());
 41:                     continueConsume = false;
 42:                 } else {
 43:                     commitOffset = consumeRequest.getProcessQueue().commit();
 44:                 }
 45:                 break;
 46:             default:
 47:                 break;
 48:         }
 49:     } else {
 50:         switch (status) {
 51:             case SUCCESS:
 52:                 this.getConsumerStatsManager().incConsumeOKTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
 53:                 break;
 54:             case COMMIT:
 55:                 // 提交消息已消费成功到消息处理队列
 56:                 commitOffset = consumeRequest.getProcessQueue().commit();
 57:                 break;
 58:             case ROLLBACK:
 59:                 // 设置消息重新消费
 60:                 consumeRequest.getProcessQueue().rollback();
 61:                 this.submitConsumeRequestLater(//
 62:                     consumeRequest.getProcessQueue(), //
 63:                     consumeRequest.getMessageQueue(), //
 64:                     context.getSuspendCurrentQueueTimeMillis());
 65:                 continueConsume = false;
 66:                 break;
 67:             case SUSPEND_CURRENT_QUEUE_A_MOMENT: // 计算是否暂时挂起(暂停)消费N毫秒,默认:10ms
 68:                 this.getConsumerStatsManager().incConsumeFailedTPS(consumerGroup, consumeRequest.getMessageQueue().getTopic(), msgs.size());
 69:                 if (checkReconsumeTimes(msgs)) {
 70:                     // 设置消息重新消费
 71:                     consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs);
 72:                     // 提交延迟消费请求
 73:                     this.submitConsumeRequestLater(//
 74:                         consumeRequest.getProcessQueue(), //
 75:                         consumeRequest.getMessageQueue(), //
 76:                         context.getSuspendCurrentQueueTimeMillis());
 77:                     continueConsume = false;
 78:                 }
 79:                 break;
 80:             default:
 81:                 break;
 82:         }
 83:     }
 84: 
 85:     // 消息处理队列未dropped,提交有效消费进度
 86:     if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
 87:         this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);
 88:     }
 89: 
 90:     return continueConsume;
 91: }
 92: 
 93: private int getMaxReconsumeTimes() {
 94:     // default reconsume times: Integer.MAX_VALUE
 95:     if (this.defaultMQPushConsumer.getMaxReconsumeTimes() == -1) {
 96:         return Integer.MAX_VALUE;
 97:     } else {
 98:         return this.defaultMQPushConsumer.getMaxReconsumeTimes();
 99:     }
100: }
101: 
102: /**
103:  * 计算是否要暂停消费
104:  * 不暂停条件:存在消息都超过最大消费次数并且都发回broker成功
105:  *
106:  * @param msgs 消息
107:  * @return 是否要暂停
108:  */
109: private boolean checkReconsumeTimes(List<MessageExt> msgs) {
110:     boolean suspend = false;
111:     if (msgs != null && !msgs.isEmpty()) {
112:         for (MessageExt msg : msgs) {
113:             if (msg.getReconsumeTimes() >= getMaxReconsumeTimes()) {
114:                 MessageAccessor.setReconsumeTime(msg, String.valueOf(msg.getReconsumeTimes()));
115:                 if (!sendMessageBack(msg)) { // 发回失败,中断
116:                     suspend = true;
117:                     msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
118:                 }
119:             } else {
120:                 suspend = true;
121:                 msg.setReconsumeTimes(msg.getReconsumeTimes() + 1);
122:             }
123:         }
124:     }
125:     return suspend;
126: }
127: 
128: /**
129:  * 发回消息。
130:  * 消息发回broker后,对应的消息队列是死信队列。
131:  *
132:  * @param msg 消息
133:  * @return 是否发送成功
134:  */
135: public boolean sendMessageBack(final MessageExt msg) {
136:     try {
137:         // max reconsume times exceeded then send to dead letter queue.
138:         Message newMsg = new Message(MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()), msg.getBody());
139:         String originMsgId = MessageAccessor.getOriginMessageId(msg);
140:         MessageAccessor.setOriginMessageId(newMsg, UtilAll.isBlank(originMsgId) ? msg.getMsgId() : originMsgId);
141:         newMsg.setFlag(msg.getFlag());
142:         MessageAccessor.setProperties(newMsg, msg.getProperties());
143:         MessageAccessor.putProperty(newMsg, MessageConst.PROPERTY_RETRY_TOPIC, msg.getTopic());
144:         MessageAccessor.setReconsumeTime(newMsg, String.valueOf(msg.getReconsumeTimes()));
145:         MessageAccessor.setMaxReconsumeTimes(newMsg, String.valueOf(getMaxReconsumeTimes()));
146:         newMsg.setDelayTimeLevel(3 + msg.getReconsumeTimes());
147: 
148:         this.defaultMQPushConsumer.getDefaultMQPushConsumerImpl().getmQClientFactory().getDefaultMQProducer().send(newMsg);
149:         return true;
150:     } catch (Exception e) {
151:         log.error("sendMessageBack exception, group: " + this.consumerGroup + " msg: " + msg.toString(), e);
152:     }
153: 
154:     return false;
155: }
  • 第 21 至 29 行 :消费成功。在自动提交进度( AutoCommit )的情况下,COMMIT、ROLLBACK、SUCCESS 逻辑已经统一。
  • 第 30 至 45 行 :消费失败。当消息重试次数超过上限(默认 :16次)时,将消息发送到 Broker 死信队列,跳过这些消息。此时,消息队列无需挂起,继续消费后面的消息。
  • 第 85 至 88 行 :提交消费进度。

总结

1)检查消息的重试次数。如果消息重试次数大于或等于允许的最大重试次数,将该消息发送到Broker端,该消息在消息服务端最终会进入到DLQ(死信队列),也就是RocketMQ不会再次消费,需要人工干预。如果消息成功进入到DLQ队列,checkReconsumeTimes返回false,该批消息将直接调用ProcessQueue#commit提交,表示消息消费成功,如果这批消息中有任意一条消息的重试次数小于允许的最大重试次数,将返回true,执行消息重试。

2)消息消费重试,先将该批消息重新放入到ProcessQueue的msgTreeMap,然后清除consumingMsgOrderlyTreeMap,默认延迟1s再加入到消费队列中,并结束此次消息消费。可以通过DefaultMQPushConsumer#setSuspendCurrentQueueTimeMillis设置当前队列重试挂起时间。如果执行消息重试,因为消息消费进度并未向前推进,故本次视为无效消费,将不更新消息消费进度。

你可能感兴趣的:(rocketmq)