mq支持局部消息顺序消费,可以确保同一个消息消费队列中的消息被顺序消费。看下针对顺序消息在整个消费过程中做的调整:
队列负载:
DefaultMQPushConsumerImpl#consumeOrderly决定是否是顺序消息,
org.apache.rocketmq.client.impl.consumer.RebalanceImpl#rebalanceByTopic:
在新分配到队列时,新添加消息拉取任务之前会先检查是否是顺序消息。如果是顺序消息检查上锁是否成功:
消息拉取:
DefaultMQPushConsumerImpl#pullMessage:
如果是顺序消息但队列未被锁定,则延迟3s后再将pullRequest对象放入到拉取任务中,如果该处理队列是第一次拉取任务,则首先计算拉取偏移量,然后向消息服务端拉取消息。
消息消费:
public class ConsumeMessageOrderlyService implements ConsumeMessageService { private static final InternalLogger log = ClientLogger.getLog(); private final static long MAX_TIME_CONSUME_CONTINUOUSLY = Long.parseLong(System.getProperty("rocketmq.client.maxTimeConsumeContinuously", "60000"));//每次消费任务最大持续时间,默认60秒 private final DefaultMQPushConsumerImpl defaultMQPushConsumerImpl;//消息消费者实现类 private final DefaultMQPushConsumer defaultMQPushConsumer;//消息消费者 private final MessageListenerOrderly messageListener;//顺序消息消费监听器 private final BlockingQueue<Runnable> consumeRequestQueue;//消息消费任务队列 private final ThreadPoolExecutor consumeExecutor;//消息消费线程池 private final String consumerGroup;//消息组名 private final MessageQueueLock messageQueueLock = new MessageQueueLock();//消息消费端消息队列锁容器 private final ScheduledExecutorService scheduledExecutorService;//调度任务线程池 private volatile boolean stopped = false;
public ConsumeMessageOrderlyService(DefaultMQPushConsumerImpl defaultMQPushConsumerImpl, MessageListenerOrderly messageListener) { this.defaultMQPushConsumerImpl = defaultMQPushConsumerImpl; this.messageListener = messageListener; this.defaultMQPushConsumer = this.defaultMQPushConsumerImpl.getDefaultMQPushConsumer(); this.consumerGroup = this.defaultMQPushConsumer.getConsumerGroup(); this.consumeRequestQueue = new LinkedBlockingQueue<Runnable>(); this.consumeExecutor = new ThreadPoolExecutor( this.defaultMQPushConsumer.getConsumeThreadMin(), this.defaultMQPushConsumer.getConsumeThreadMax(), 1000 * 60, TimeUnit.MILLISECONDS, this.consumeRequestQueue, new ThreadFactoryImpl("ConsumeMessageThread_")); this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("ConsumeMessageScheduledThread_")); }
注意消息任务队列为LinkedBlockingQueue。
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() { ConsumeMessageOrderlyService.this.lockMQPeriodically(); } }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS); } }
集群模式下,启动定时任务每20s执行一次锁定分配给自己的消息消费队列。具体实现过程:
将消息队列按照Broker组织成Map<String/*BrokerName*/,set<MessageQueue>>,方便下一步向Broker发送锁定消息队列请求。
向Broker(master主节点)发送锁定消息队列,该方法返回成功被当前消费者锁定的消息消费队列。
将成功锁定的消息消费队列相对应的处理队列设置为锁定状态,同时更新加锁时间。
遍历当前处理队列中的消息消费队列,如果当前消费者不持有该消费队列的锁,将处理队列锁状态设置为false,暂停该消息消费队列的消息拉取于消息消费。
3.提交消息消费任务
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#submitConsumeRequest:
顺序消息的ConsumeRequest消费任务不会直接消费本次拉取的消息,而是在消息消费时,从处理队列中拉取消息:
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService.ConsumeRequest#run:
根据消息队列获取一个对象,然后消息消费时先申请独占objLock。顺序消息消费的并发度为消息队列。也就是一个消息消费队列同一时刻只会被一个消费线程池中一个线程消费。
if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel()) || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) { //消息消费逻辑 }
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);}
如果是广播模式的话,直接进入消费,无须锁定处理队列,因为相互之间无竞争;
如果是集群模式,消息消费的前提条件是processQueue被锁定且锁未超时。思考:会不会出现当消息队列重新负载时,原先由自己处理的消息队列被另外一个消费者分配,此时如果还未来得及讲ProcessQueue解除锁定,就被另外一个消费者添加进去,此时会存在多个消息消费者同时消费一个消息队列?答案是不会的,因为当一个新的消费队列分配给消费者是,在添加其拉取任务之前必须先向Broker发送对该消息队列枷锁请求,只有加锁成功后,才能添加拉取消息,否则等到下一次负载后,只有消息队列被原先占有的消费者释放后,才能开始新的拉取任务。集群模式下,如果未锁定处理队列,则延迟该队列的消息消费。
顺序消息消费处理逻辑,每一个ConsumeRequest消费任务不是以消费消息条数来计算的,而是根据消费时间,默认当消费时长大于MAX_TIME_CONSUME_CONTINUOUSLY,默认60s后,本次消费任务结束,有消费组内其他线程继续消费。
顺序消息消费时,从ProcessQueue中取出的消息,会临时存储在ProceeQueue的consumingMsgOrderlyTreeMap属性中。
执行消息消费钩子函数.
消费成功,提交,就是将该批消息从ProcessQueue中移除,维护msgCount(消息处理队列中消息条数)并获取消息消费的偏移量offset,并返回待保存的消息消费进度,
org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#processConsumeResult:
checkReconsumeTimes发送ack失败,则本地重试,否则则认为消息消费成功 直接commit。
存储消息消费进度。
MessageQueue的锁和ProcessQueue的锁 是为了防止重新负载时出现多个消费者消费统一队列,线程池使用的对象锁,保证了consumeRequest被线程池顺序消费,但是ProcessQueue的锁什么时候unlock?(队列被丢弃移除的时候?)
来源:https://www.cnblogs.com/lccsblog/p/12293608.html