
导读
本文围绕RocketMQ和基于其实现的DDMQ的顺序消费机制展开深度探讨,从源码解读二者顺序消费的实现原理和差异,包括发送端的顺序发送,Broker及消费端的顺序拉取,消费端的顺序消费等。
适合有一定消息队列基础,正在或计划使用RocketMQ及相关衍生产品进行开发,对消息顺序性有严格要求,期望深入理解底层原理以优化系统性能和稳定性的后端开发工程师、架构师阅读。

RocketMQ

RocketMQ模型
先简单了解一下RocketMQ的模型,有个概念。
部署模型

NameServer:担任路由消息的提供者,使得生产者或消费者能够通过NameServer查找各Topic及其queue相应的Broker IP列表。
Broker:消息中转角色,负责接收从生产者发来的消息并存储,同时为消费者拉取请求做准备。
队列模型

Topic:一类消息的集合,每个topic包含若干条消息,但每条消息只能属于一个topic。
Tag:相同topic下可以有不同的tag,即再分类。
注:上图中TopicA和TopicB的Queue0不是同一个队列。

集群消费下,同一Topic下的队列会均匀分配给同一消费者组中的每位消费者。
Rebalance机制,即负载均衡机制:将队列均匀分配给消费者,包括队列数或消费者数有变化时也是通过该机制重新分配。
这里Rebalance策略有几种,由于不是本次分享重点就不展开了,感兴趣的可以看org.apache.rocketmq.client.consumer.rebalance目录下的实现。

一张图了解RocketMQ如何顺序消费


源码解读
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>5.3.1</version></dependency>
顺序发送消息
做法:按序投递至同一队列中。
调用方法:org.apache.rocketmq.client.producer.DefaultMQProducer#send(org.apache.rocketmq.common.message.Message, org.apache.rocketmq.client.producer.MessageQueueSelector, java.lang.Object)
// 顺序发送使用示例public static void main(String[] args) throws Exception { // 创建生产者实例并设置生产者组名 DefaultMQProducer producer = new DefaultMQProducer("producer_group_name"); // 设置Name Server地址 producer.setNamesrvAddr("127.0.0.1:9876"); producer.start(); // 发送消息 Message message = new Message("test_topic", "test_tag", "Hello, Rocketmq!".getBytes()); SendResult sendResult = producer.send(message, new SelectMessageQueueByHash(), "test_arg"); System.out.println(sendResult); //... producer.shutdown();}
这个send方法有三个参数
Message msg:包含Topic、Tag、消息内容。
MessageQueueSelector selector:消息队列选择器(一个interface,有一个select方法,返回是消息队列),可自行实现也可以用SDK中提供的几个。
Object arg:上面selector的select方法的参数。
// org.apache.rocketmq.client.producer.DefaultMQProducer#send(org.apache.rocketmq.common.message.Message, org.apache.rocketmq.client.producer.MessageQueueSelector, java.lang.Object)// 消息发送调用入口@Overridepublic SendResult send(Message msg, MessageQueueSelector selector, Object arg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { msg.setTopic(withNamespace(msg.getTopic())); // 选取发送的队列 MessageQueue mq = this.defaultMQProducerImpl.invokeMessageQueueSelector(msg, selector, arg, this.getSendMsgTimeout()); mq = queueWithNamespace(mq); // 执行消息发送 if (this.getAutoBatch() && !(msg instanceof MessageBatch)) { return sendByAccumulator(msg, mq, null); } else { return sendDirect(msg, mq, null); }}
// org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#invokeMessageQueueSelector// 选择器:选择发送的消息队列public MessageQueue invokeMessageQueueSelector(Message msg, MessageQueueSelector selector, Object arg, final long timeout) throws MQClientException, RemotingTooMuchRequestException { long beginStartTime = System.currentTimeMillis(); this.makeSureStateOK(); Validators.checkMessage(msg, this.defaultMQProducer); // 获取Topic对象 TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); if (topicPublishInfo != null && topicPublishInfo.ok()) { MessageQueue mq = null; try { // 获取Topic的所有队列 List<MessageQueue> messageQueueList = mQClientFactory.getMQAdminImpl().parsePublishMessageQueues(topicPublishInfo.getMessageQueueList()); Message userMessage = MessageAccessor.cloneMessage(msg); String userTopic = NamespaceUtil.withoutNamespace(userMessage.getTopic(), mQClientFactory.getClientConfig().getNamespace()); userMessage.setTopic(userTopic); // 选取发送的队列 mq = mQClientFactory.getClientConfig().queueWithNamespace(selector.select(messageQueueList, userMessage, arg)); } catch (Throwable e) { throw new MQClientException("select message queue threw exception.", e); } long costTime = System.currentTimeMillis() - beginStartTime; if (timeout < costTime) { throw new RemotingTooMuchRequestException("sendSelectImpl call timeout"); } if (mq != null) { // 返回队列 return mq; } else { throw new MQClientException("select message queue return null.", null); } } validateNameServerSetting(); throw new MQClientException("No route info for this topic, " + msg.getTopic(), null);}
SDK中提供的几种MessageQueueSelector实现

// org.apache.rocketmq.client.producer.selector.SelectMessageQueueByHash// hash取余 选择消息队列public class SelectMessageQueueByHash implements MessageQueueSelector { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { int value = arg.hashCode() % mqs.size(); if (value < 0) { value = Math.abs(value); } return mqs.get(value); }}
// org.apache.rocketmq.client.producer.selector.SelectMessageQueueByRandom// 随机 选择消息队列public class SelectMessageQueueByRandom implements MessageQueueSelector { private Random random = new Random(System.currentTimeMillis()); @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { int value = random.nextInt(mqs.size()); return mqs.get(value); }}
所以,消息顺序发送的重点就是这个selector,要确保消息都发送到了同一个队列中。
在消息队列数量不变的情况下,是可以使用hash取余的这种方法的,同时还需保证消息发送时传入的arg不变。一般默认也是用的这种方法。
另外,若有并发问题或多实例同时投递问题还需要通过加锁等方式自行控制消息按序发送。
顺序消费消息

集群消费下,保证消费者集群只有一位在拉取及消费消息。
消费者消费时只有一个线程在消费;
// 客户端顺序消费使用示例public static void main(String[] args) throws Exception { // 创建消费者实例并设置消费者组名和消费模式 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group_name"); consumer.setNamesrvAddr("127.0.0.1:9876"); consumer.setMessageModel(MessageModel.CLUSTERING); consumer.subscribe("test_topic", "test_tag"); // 设置顺序消费消息监听器 consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { for (MessageExt msg : msgs) { try { System.out.println(new String(msg.getBody())); } catch (Exception e) { e.printStackTrace(); } } return ConsumeOrderlyStatus.SUCCESS; } }); consumer.start();}
客户端-拉取消息
拉取消息前,请求Broker端上锁,定时20秒执行一次续锁:
// org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#start// 拉取消息-客户端请求Broker端上锁@Overridepublic void start() { // 集群消费模式下,通过定时器,每20s请求一次Broker上锁 if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) { this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { ConsumeMessageOrderlyService.this.lockMQPeriodically(); } catch (Throwable e) { log.error("scheduleAtFixedRate lockMQPeriodically exception", e); } } // REBALANCE_LOCK_INTERVAL 默认值为20000,可配置 }, 1000, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS); }}
// org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService#lockMQPeriodicallypublic synchronized void lockMQPeriodically() { if (!this.stopped) { this.defaultMQPushConsumerImpl.getRebalanceImpl().lockAll(); }}
// org.apache.rocketmq.client.impl.consumer.RebalanceImpl#lockAllpublic void lockAll() { 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; } // 获取Broker FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(brokerName, MixAll.MASTER_ID, true); if (findBrokerResult != null) { // 通过 消费者组 + clientId 对 messageQueue 进行Broker端上锁 LockBatchRequestBody requestBody = new LockBatchRequestBody(); requestBody.setConsumerGroup(this.consumerGroup); requestBody.setClientId(this.mQClientFactory.getClientId()); requestBody.setMqSet(mqs); try { // 尝试上锁。这里的1s不是锁的过期时间,是请求超时时间;锁过期时间是维护在Broker端 Set<MessageQueue> lockOKMQSet = this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000); for (MessageQueue mq : mqs) { ProcessQueue processQueue = this.processQueueTable.get(mq); if (processQueue != null) { if (lockOKMQSet.contains(mq)) { if (!processQueue.isLocked()) { log.info("the message queue locked OK, Group: {} {}", this.consumerGroup, mq); } // 上锁成功 processQueue.setLocked(true); processQueue.setLastLockTimestamp(System.currentTimeMillis()); } else { // 上锁失败 processQueue.setLocked(false); log.warn("the message queue locked Failed, Group: {} {}", this.consumerGroup, mq); } } } }&

最低0.47元/天 解锁文章
517

被折叠的 条评论
为什么被折叠?



