RocketMQ源码分析之顺序消费

本文深入分析RocketMQ的顺序消费机制,包括Consumer如何通过分布式锁确保消息的严格顺序消费,以及在消费过程中涉及的关键步骤和处理策略,如消息队列的获取与移除、消费结果的处理等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

RocketMQ按照顺序消费有两种顺序级别,一种是普通顺序消息。另外一种是更完全严格顺序

  • 普通顺序消息指的是Producer将消息发送到相对应的消息队列上面
  • 完全严格顺序:在普通顺序消息的基础上,Consumer严格进行顺序消费
    在绝大部分情况下只需要用到普通顺序消息,大部分的应用都能容忍短暂的乱序;官方文档给出的说明中表示目前只有数据库的Binlog同步会强依赖完全严格顺序(要保证数据库事务的ACID特性)。

Comsumer严格顺序消费

Consumer在进行严格顺序消费的时候,需要利用到三把锁,Broker消息队列锁——>Consumer消息队列锁——>Consumer消息处理队列消费锁,锁粒度越来越细
- Broker消息队列锁是一个分布式锁,在集群模式下需要,在广播模式下不需要,集群模式中,只有获得该锁才能对Broker中的消息队列进行消息拉取操作
- Consumer消息队列锁是一个Broker本地锁,只有Consumer获得该锁后才能操作消息队列
- Consumer
- 消息处理队列消费锁是消费者客户端的一个本地锁,只有Consumer获得该锁后才能对消息处理队列进行消息的消费。

获取分布式Broker消息队列锁【RebalanceImpl.java#updateProcessQueueTableInRebalance】

在集群分布式模式下,Broker会不断接收消息队列的更新请求,Broker就会通过更新操作检查这个消息队列是不是属于自己,具体就是通过锁定这个分布式锁实现的,如果锁获取成功,表示这个消息队列是属于自己的,允许进行消息拉取,如果获取锁失败,则不允许进行消息拉取。
Broker消息队列分布式锁默认30s会过期,因此Consumer需要不断刷新该锁的过期时间,默认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这个消息队列不需要其他的Consumer来消费)RebalancePushImpl.java#removeUnnecessaryMessageQueue

在集群模式下,为了避免其它Consuer在获取分布式锁时和消息队列的消费冲突,如果获取锁失败,进行移除消息队列将会失败,等到下次重新分配消费队列的时候,再进行移除;如果在没有获取分布式锁的情况下就进行消息队列移除,那么可能会导致当前Consumer和其他的Consumer同时消费该消息队列,这样将恶恶法保证消息按照完全严格顺序消费。
在解锁Broker消息队列锁的时候,如果消息队列存在剩下没被拉取的消息,则进行延迟解锁【RebanlancePushImpl.java#unlockDelay】Broker消息队列锁。因为这样能保证还没被拉取的消息能被全部拉取进行消费,这样才能保证消息被完全严格顺序消费。

消费消息队列

消费消息时序图

这里写图片描述

// ⬇️⬇️⬇️【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: }
处理消费结果

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

  • SUCCESS:消费成功但不提交
  • ROLLBACK:消费失败,并进行消费回滚
  • COMMIT:消费成功并提交
  • SUSPEND_CURRENT_QUEUE_A_MOMENT:消费失败并挂起消费
    ROLLBACK、COMMIT只有在事务消息下(Binlog)使用,被官方标记为@Deprecated
    在并发场景中,如果消费失败,Consuner会将消f失败消息发回到Broker重试队列中。然后跳过当前消息,等到下次拉取该消息再进行消费。
    不过消费失败的消息一直失败,也不可能一直消费。当超过消费重试上限时,Consumer 会将消费失败超过上限的消息发回到 Broker 死信队列。默认16次,最后会提交消费进度

在完全严格顺序消费中,如果消费失败,就挂起队列一会儿,稍后继续消费

消息处理队列核心方法
 1: // ⬇️⬇️⬇️【ProcessQueue.java】
  2: /**
  3:  * 消息映射
  4:  * key:消息队列位置
  5:  */
  6: private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<>();    /**
  7:  * 消息映射临时存储(消费中的消息)
  8:  */
  9: private final TreeMap<Long, MessageExt> msgTreeMapTemp = new TreeMap<>();
 10: 
 11: /**
 12:  * 回滚消费中的消息
 13:  * 逻辑类似于{@link #makeMessageToCosumeAgain(List)}
 14:  */
 15: public void rollback() {
 16:     try {
 17:         this.lockTreeMap.writeLock().lockInterruptibly();
 18:         try {
 19:             this.msgTreeMap.putAll(this.msgTreeMapTemp);
 20:             this.msgTreeMapTemp.clear();
 21:         } finally {
 22:             this.lockTreeMap.writeLock().unlock();
 23:         }
 24:     } catch (InterruptedException e) {
 25:         log.error("rollback exception", e);
 26:     }
 27: }
 28: 
 29: /**
 30:  * 提交消费中的消息已消费成功,返回消费进度
 31:  *
 32:  * @return 消费进度
 33:  */
 34: public long commit() {
 35:     try {
 36:         this.lockTreeMap.writeLock().lockInterruptibly();
 37:         try {
 38:             // 消费进度
 39:             Long offset = this.msgTreeMapTemp.lastKey();
 40: 
 41:             //
 42:             msgCount.addAndGet(this.msgTreeMapTemp.size() * (-1));
 43: 
 44:             //
 45:             this.msgTreeMapTemp.clear();
 46: 
 47:             // 返回消费进度
 48:             if (offset != null) {
 49:                 return offset + 1;
 50:             }
 51:         } finally {
 52:             this.lockTreeMap.writeLock().unlock();
 53:         }
 54:     } catch (InterruptedException e) {
 55:         log.error("commit exception", e);
 56:     }
 57: 
 58:     return -1;
 59: }
 60: 
 61: /**
 62:  * 指定消息重新消费
 63:  * 逻辑类似于{@link #rollback()}
 64:  *
 65:  * @param msgs 消息
 66:  */
 67: public void makeMessageToCosumeAgain(List<MessageExt> msgs) {
 68:     try {
 69:         this.lockTreeMap.writeLock().lockInterruptibly();
 70:         try {
 71:             for (MessageExt msg : msgs) {
 72:                 this.msgTreeMapTemp.remove(msg.getQueueOffset());
 73:                 this.msgTreeMap.put(msg.getQueueOffset(), msg);
 74:             }
 75:         } finally {
 76:             this.lockTreeMap.writeLock().unlock();
 77:         }
 78:     } catch (InterruptedException e) {
 79:         log.error("makeMessageToCosumeAgain exception", e);
 80:     }
 81: }
 82: 
 83: /**
 84:  * 获得持有消息前N条
 85:  *
 86:  * @param batchSize 条数
 87:  * @return 消息
 88:  */
 89: public List<MessageExt> takeMessags(final int batchSize) {
 90:     List<MessageExt> result = new ArrayList<>(batchSize);
 91:     final long now = System.currentTimeMillis();
 92:     try {
 93:         this.lockTreeMap.writeLock().lockInterruptibly();
 94:         this.lastConsumeTimestamp = now;
 95:         try {
 96:             if (!this.msgTreeMap.isEmpty()) {
 97:                 for (int i = 0; i < batchSize; i++) {
 98:                     Map.Entry<Long, MessageExt> entry = this.msgTreeMap.pollFirstEntry();
 99:                     if (entry != null) {
100:                         result.add(entry.getValue());
101:                         msgTreeMapTemp.put(entry.getKey(), entry.getValue());
102:                     } else {
103:                         break;
104:                     }
105:                 }
106:             }
107: 
108:             if (result.isEmpty()) {
109:                 consuming = false;
110:             }
111:         } finally {
112:             this.lockTreeMap.writeLock().unlock();
113:         }
114:     } catch (InterruptedException e) {
115:         log.error("take Messages exception", e);
116:     }
117: 
118:     return result;
119: }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值