您现在的位置是:亿华云 > 人工智能
顺序消息的实现-RocketMQ知识体系(五)
亿华云2025-10-03 02:20:30【人工智能】7人已围观
简介我们知道,kafka 如果要保证顺序消费,必须保证消息保存到同一个patition上,而且为了有序性,只能有一个消费者进行消费。这种情况下,Kafka 就退化成了单一队列,毫无并发性可言,极大降低系统
我们知道,顺序识体kafka 如果要保证顺序消费,消息现R系必须保证消息保存到同一个patition上,顺序识体而且为了有序性,消息现R系只能有一个消费者进行消费。顺序识体这种情况下,消息现R系Kafka 就退化成了单一队列,顺序识体毫无并发性可言,消息现R系极大降低系统性能。顺序识体那么对于对业务比较友好的消息现R系RocketMQ 是如何实现的呢?首先,我们循序渐进的顺序识体来了解下顺序消息的实现。
顺序消息业务使用场景
1、消息现R系电商场景中传递订单状态。顺序识体
2、消息现R系同步mysql 的顺序识体binlong 日志,数据库的操作是有顺序的。
3、其他消息之间有先后的依赖关系,后一条消息需要依赖于前一条消息的处理结果的情况。
等等。。。服务器托管
消息中间件中的顺序消息
顺序消息(FIFO 消息)是 MQ 提供的一种严格按照顺序进行发布和消费的消息类型。顺序消息由两个部分组成:顺序发布和顺序消费。
顺序消息包含两种类型:
分区顺序:一个Partition(queue)内所有的消息按照先进先出的顺序进行发布和消费
全局顺序:一个Topic内所有的消息按照先进先出的顺序进行发布和消费.但是全局顺序极大的降低了系统的吞吐量,不符合mq的设计初衷。
那么折中的办法就是选择分区顺序。
【局部顺序消费】
如何保证顺序
在MQ的模型中,顺序需要由3个阶段去保障:
消息被发送时保持顺序 消息被存储时保持和发送的顺序一致 消息被消费时保持和存储的顺序一致发送时保持顺序意味着对于有顺序要求的消息,用户应该在同一个线程中采用同步的方式发送。存储保持和发送的顺序一致则要求在同一线程中被发送出来的消息A和B,存储时在空间上A一定在B之前。而消费保持和存储一致则要求消息A、B到达Consumer之后必须按照先A后B的源码库顺序被处理。
第一点,消息顺序发送,多线程发送的消息无法保证有序性,因此,需要业务方在发送时,针对同一个业务编号(如同一笔订单)的消息需要保证在一个线程内顺序发送,在上一个消息发送成功后,在进行下一个消息的发送。对应到mq中,消息发送方法就得使用同步发送,异步发送无法保证顺序性。
第二点,消息顺序存储,mq的topic下会存在多个queue,要保证消息的顺序存储,同一个业务编号的消息需要被发送到一个queue中。对应到mq中,需要使用MessageQueueSelector来选择要发送的queue,即对业务编号进行hash,然后根据队列数量对hash值取余,将消息发送到一个queue中。
第三点,消息顺序消费,要保证消息顺序消费,同一个queue就只能被一个消费者所消费,高防服务器因此对broker中消费队列加锁是无法避免的。同一时刻,一个消费队列只能被一个消费者消费,消费者内部,也只能有一个消费线程来消费该队列。即,同一时刻,一个消费队列只能被一个消费者中的一个线程消费。
RocketMQ中顺序的实现
【Producer端】
Producer端确保消息顺序唯一要做的事情就是将消息路由到特定的分区,在RocketMQ中,通过MessageQueueSelector来实现分区的选择。
/** * 消息队列选择器 */ public interface MessageQueueSelector { /** * 选择消息队列 * * @param mqs 消息队列 * @param msg 消息 * @param arg 参数 * @return 消息队列 */ MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg); } List mqs:消息要发送的Topic下所有的分区 Message msg:消息对象 额外的参数:用户可以传递自己的参数比如如下实现就可以保证相同的订单的消息被路由到相同的分区:
long orderId = ((Order) object).getOrderId; return mqs.get(orderId % mqs.size());【Consumer端】
尝试锁定锁定MessageQueue。
首先我们如何保证一个队列只被一个消费者消费?
消费队列存在于broker端,如果想保证一个队列被一个消费者消费,那么消费者在进行消息拉取消费时就必须向mq服务器申请队列锁,消费者申请队列锁的代码存在于RebalanceService消息队列负载的实现代码中。
消费者重新负载,并且分配完消费队列后,需要向mq服务器发起消息拉取请求,代码实现在RebalanceImpl#updateProcessQueueTableInRebalance中,针对顺序消息的消息拉取,mq做了如下判断:
// 增加 不在processQueueTable && 存在于mqSet 里的消息队列。 List<PullRequest> pullRequestList = new ArrayList<>(); // 拉消息请求数组 for (MessageQueue mq : mqSet) { if (!this.processQueueTable.containsKey(mq)) { if (isOrder && !this.lock(mq)) { // 顺序消息锁定消息队列 log.warn("doRebalance, { }, add a new mq failed, { }, because lock failed", consumerGroup, mq); continue; } this.removeDirtyOffset(mq); ProcessQueue pq = new ProcessQueue(); long nextOffset = this.computePullFromWhere(mq); 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; } } else { log.warn("doRebalance, { }, add new mq failed, { }", consumerGroup, mq); } } } // 发起消息拉取请求 this.dispatchPullRequest(pullRequestList);核心思想就是,消费客户端先向broker端发起对messageQueue的加锁请求,只有加锁成功时才创建pullRequest进行消息拉取,下面看下lock加锁请求方法:
/** * 请求Broker获得指定消息队列的分布式锁 * * @param mq 队列 * @return 是否成功 */ public boolean lock(final MessageQueue mq) { 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()); requestBody.getMqSet().add(mq); try { // 请求Broker获得指定消息队列的分布式锁 Set<MessageQueue> lockedMq = this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000); // 设置消息处理队列锁定成功。锁定消息队列成功,可能本地没有消息处理队列,设置锁定成功会在lockAll()方法。 for (MessageQueue mmqq : lockedMq) { ProcessQueue processQueue = this.processQueueTable.get(mmqq); if (processQueue != null) { processQueue.setLocked(true); processQueue.setLastLockTimestamp(System.currentTimeMillis()); } } 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; }代码实现逻辑比较清晰,就是调用lockBatchMQ方法发送了一个加锁请求,那么broker端收到加锁请求后的处理逻辑又是怎么样?
【broker端实现】
broker端收到加锁请求的处理逻辑在RebalanceLockManager#tryLockBatch方法中,RebalanceLockManager中关键属性如下:
/** * 消息队列锁过期时间,默认60s */ private final static long REBALANCE_LOCK_MAX_LIVE_TIME = Long.parseLong(System.getProperty( "rocketmq.broker.rebalance.lockMaxLiveTime", "60000")); /** * 锁 */ private final Lock lock = new ReentrantLock(); /** * 消费分组的消息队列锁映射 */ private final ConcurrentHashMap<String/* group */, ConcurrentHashMap<MessageQueue, LockEntry>> mqLockTable = new ConcurrentHashMap<>(1024);LockEntry对象中关键属性如下:
/** * 锁定记录 */ static class LockEntry { /** * 客户端编号 */ private String clientId; /** * 最后锁定时间 */ private volatile long lastUpdateTimestamp = System.currentTimeMillis(); public String getClientId() { return clientId; } public void setClientId(String clientId) { this.clientId = clientId; } public long getLastUpdateTimestamp() { return lastUpdateTimestamp; } public void setLastUpdateTimestamp(long lastUpdateTimestamp) { this.lastUpdateTimestamp = lastUpdateTimestamp; } /** * 是否锁定 * * @param clientId 客户端编号 * @return 是否 */ public boolean isLocked(final String clientId) { boolean eq = this.clientId.equals(clientId); return eq && !this.isExpired(); } /** * 锁定是否过期 * * @return 是否 */ public boolean isExpired() { boolean expired = (System.currentTimeMillis() - this.lastUpdateTimestamp) > REBALANCE_LOCK_MAX_LIVE_TIME; return expired; } }broker端通过对ConcurrentMap> mqLockTable的维护来达到messageQueue加锁的目的,使得同一时刻,一个messageQueue只能被一个消费者消费。
【再次回到Consumer端,拿到锁后】
消费者对messageQueue的加锁已经成功,那么就进入到了第二个步骤,创建pullRequest进行消息拉取,消息拉取部分的代码实现在PullMessageService中,消息拉取完后,需要提交到ConsumeMessageService中进行消费,顺序消费的实现为ConsumeMessageOrderlyService,提交消息进行消费的方法为ConsumeMessageOrderlyService#submitConsumeRequest,具体实现如下:
@Override 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); } }构建了一个ConsumeRequest对象,并提交给了ThreadPoolExecutor来并行消费,看下顺序消费的ConsumeRequest的run方法实现:
public void run() { if (this.processQueue.isDropped()) { log.warn("run, the message queue not be able to consume, because its dropped. { }", this.messageQueue); return; } // 获得 Consumer 消息队列锁 final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue); synchronized (objLock) { // (广播模式) 或者 (集群模式 && Broker消息队列锁有效) if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel()) || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) { final long beginTime = System.currentTimeMillis(); // 循环 for (boolean continueConsume = true; continueConsume; ) { if (this.processQueue.isDropped()) { log.warn("the message queue not be able to consume, because its 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; } // 当前周期消费时间超过连续时长,默认:60s,提交延迟消费请求。默认情况下,每消费1分钟休息10ms。 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(); List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize); if (!msgs.isEmpty()) { final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue); ConsumeOrderlyStatus status = null; // Hook:before ConsumeMessageContext consumeMessageContext = null; if (ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.hasHook()) { consumeMessageContext = new ConsumeMessageContext(); consumeMessageContext .setConsumerGroup(ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumerGroup()); 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 its 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; 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; } // Hook:after 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 its dropped. { }", this.messageQueue); return; } ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100); } } }获取到锁对象后,使用synchronized尝试申请线程级独占锁。
如果加锁成功,同一时刻只有一个线程进行消息消费。
如果加锁失败,会延迟100ms重新尝试向broker端申请锁定messageQueue,锁定成功后重新提交消费请求
至此,第三个关键点的解决思路也清晰了,基本上就两个步骤。
创建消息拉取任务时,消息客户端向broker端申请锁定MessageQueue,使得一个MessageQueue同一个时刻只能被一个消费客户端消费。
消息消费时,多线程针对同一个消息队列的消费先尝试使用synchronized申请独占锁,加锁成功才能进行消费,使得一个MessageQueue同一个时刻只能被一个消费客户端中一个线程消费。
【顺序消费问题拆解】
broke 上要保证一个队列只有一个进程消费,即一个队列同一时间只有一个consumer 消费 broker 给consumer 的消息顺序应该保持一致,这个通过 rpc传输,序列化后消息顺序不变,所以很容易实现 consumer 上的队列消息要保证同一个时间只有一个线程消费通过问题的拆分,问题变成同一个共享资源串行处理了,要解决这个问题,通常的做法都是访问资源的时候加锁,即broker 上一个队列消息在被consumer 访问的必须加锁,单个consumer 端多线程并发处理消息的时候需要加锁;这里还需要考虑broker 锁的异常情况,假如一个broke 队列上的消息被consumer 锁住了,万一consumer 崩溃了,这个锁就释放不了,所以broker 上的锁需要加上锁的过期时间。
实际上 RocketMQ 消费端也就是照着上面的思路做:
RocketMQ中顺序消息注意事项
实际项目中并不是所有情况都需要用到顺序消息,但这也是设计方案的时候容易忽略的一点
顺序消息是生产者和消费者配合协调作用的结果,但是消费端保证顺序消费,是保证不了顺序消息的
消费端并行方式消费,只设置一次拉取消息的数量为 1(即配置参数 consumeBatchSize ),是否可以实现顺序消费 ?这里实际是不能的,并发消费在消费端有多个线程同时消费,consumeBatchSize 只是一个线程一次拉取消息的数量,对顺序消费没有意义,这里大家有兴趣可以看 ConsumeMessageConcurrentlyService 的代码,并发消费的逻辑都在哪里。
在使用顺序消息时,一定要注意其异常情况的出现,对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 版会自动不断地进行消息重试(每次间隔时间为 1 秒),重试最大值是Integer.MAX_VALUE.这时,应用会出现消息消费被阻塞的情况。因此,建议您使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
重要的事再强调一次:在使用顺序消息时,一定要注意其异常情况的出现!防止资源不释放!
小结
通过以上的了解,我们知道了实现顺序消息所必要的条件:顺序发送、顺序存储、顺序消费。RocketMQ的设计中考虑到了这些,我们只需要简单的使用API,不需要额外使用代码来约束业务,使得实现顺序消息更加简单。
很赞哦!(558)