找回密码
 立即注册
首页 业界区 业界 RocketMQ源码详解(消息存储、Consumer)

RocketMQ源码详解(消息存储、Consumer)

全愉婉 2025-6-2 23:56:28
消息存储

消息存储核心类

1.png
  1. private final MessageStoreConfig messageStoreConfig;        //消息配置属性
  2. private final CommitLog commitLog;                //CommitLog文件存储的实现类
  3. private final ConcurrentMap<String/* topic */, ConcurrentMap<Integer/* queueId */, ConsumeQueue>> consumeQueueTable;        //消息队列存储缓存表,按照消息主题分组
  4. private final FlushConsumeQueueService flushConsumeQueueService;        //消息队列文件刷盘线程
  5. private final CleanCommitLogService cleanCommitLogService;        //清除CommitLog文件服务
  6. private final CleanConsumeQueueService cleanConsumeQueueService;        //清除ConsumerQueue队列文件服务
  7. private final IndexService indexService;        //索引实现类
  8. private final AllocateMappedFileService allocateMappedFileService;        //MappedFile分配服务
  9. private final ReputMessageService reputMessageService;//CommitLog消息分发,根据CommitLog文件构建ConsumerQueue、IndexFile文件
  10. private final HAService haService;        //存储HA机制
  11. private final ScheduleMessageService scheduleMessageService;        //消息服务调度线程
  12. private final StoreStatsService storeStatsService;        //消息存储服务
  13. private final TransientStorePool transientStorePool;        //消息堆外内存缓存
  14. private final BrokerStatsManager brokerStatsManager;        //Broker状态管理器
  15. private final MessageArrivingListener messageArrivingListener;        //消息拉取长轮询模式消息达到监听器
  16. private final BrokerConfig brokerConfig;        //Broker配置类
  17. private StoreCheckpoint storeCheckpoint;        //文件刷盘监测点
  18. private final LinkedList<CommitLogDispatcher> dispatcherList;        //CommitLog文件转发请求
复制代码
消息存储流程

2.png

消息存储入口:DefaultMessageStore#putMessage
  1. //判断Broker角色如果是从节点,则无需写入
  2. if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
  3.         long value = this.printTimes.getAndIncrement();
  4.         if ((value % 50000) == 0) {
  5.             log.warn("message store is slave mode, so putMessage is forbidden ");
  6.         }
  7.     return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
  8. }
  9. //判断当前写入状态如果是正在写入,则不能继续
  10. if (!this.runningFlags.isWriteable()) {
  11.         long value = this.printTimes.getAndIncrement();
  12.             return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
  13. } else {
  14.     this.printTimes.set(0);
  15. }
  16. //判断消息主题长度是否超过最大限制
  17. if (msg.getTopic().length() > Byte.MAX_VALUE) {
  18.     log.warn("putMessage message topic length too long " + msg.getTopic().length());
  19.     return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
  20. }
  21. //判断消息属性长度是否超过限制
  22. if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) {
  23.     log.warn("putMessage message properties length too long " + msg.getPropertiesString().length());
  24.     return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null);
  25. }
  26. //判断系统PageCache缓存去是否占用
  27. if (this.isOSPageCacheBusy()) {
  28.     return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
  29. }
  30. //将消息写入CommitLog文件
  31. PutMessageResult result = this.commitLog.putMessage(msg);
复制代码
代码:CommitLog#putMessage
  1. //记录消息存储时间
  2. msg.setStoreTimestamp(beginLockTimestamp);
  3. //判断如果mappedFile如果为空或者已满,创建新的mappedFile文件
  4. if (null == mappedFile || mappedFile.isFull()) {
  5.     mappedFile = this.mappedFileQueue.getLastMappedFile(0);
  6. }
  7. //如果创建失败,直接返回
  8. if (null == mappedFile) {
  9.     log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
  10.     beginTimeInLock = 0;
  11.     return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
  12. }
  13. //写入消息到mappedFile中
  14. result = mappedFile.appendMessage(msg, this.appendMessageCallback);
复制代码
代码:MappedFile#appendMessagesInner
  1. //获得文件的写入指针
  2. int currentPos = this.wrotePosition.get();
  3. //如果指针大于文件大小则直接返回
  4. if (currentPos < this.fileSize) {
  5.     //通过writeBuffer.slice()创建一个与MappedFile共享的内存区,并设置position为当前指针
  6.     ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
  7.     byteBuffer.position(currentPos);
  8.     AppendMessageResult result = null;
  9.     if (messageExt instanceof MessageExtBrokerInner) {
  10.                //通过回调方法写入
  11.         result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
  12.     } else if (messageExt instanceof MessageExtBatch) {
  13.         result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
  14.     } else {
  15.         return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
  16.     }
  17.     this.wrotePosition.addAndGet(result.getWroteBytes());
  18.     this.storeTimestamp = result.getStoreTimestamp();
  19.     return result;
  20. }
复制代码
代码:CommitLog#doAppend
  1. //文件写入位置
  2. long wroteOffset = fileFromOffset + byteBuffer.position();
  3. //设置消息ID
  4. this.resetByteBuffer(hostHolder, 8);
  5. String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);
  6. //获得该消息在消息队列中的偏移量
  7. keyBuilder.setLength(0);
  8. keyBuilder.append(msgInner.getTopic());
  9. keyBuilder.append('-');
  10. keyBuilder.append(msgInner.getQueueId());
  11. String key = keyBuilder.toString();
  12. Long queueOffset = CommitLog.this.topicQueueTable.get(key);
  13. if (null == queueOffset) {
  14.     queueOffset = 0L;
  15.     CommitLog.this.topicQueueTable.put(key, queueOffset);
  16. }
  17. //获得消息属性长度
  18. final byte[] propertiesData =msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);
  19. final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;
  20. if (propertiesLength > Short.MAX_VALUE) {
  21.     log.warn("putMessage message properties length too long. length={}", propertiesData.length);
  22.     return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED);
  23. }
  24. //获得消息主题大小
  25. final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
  26. final int topicLength = topicData.length;
  27. //获得消息体大小
  28. final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;
  29. //计算消息总长度
  30. final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength);
复制代码
代码:CommitLog#calMsgLength
  1. protected static int calMsgLength(int bodyLength, int topicLength, int propertiesLength) {
  2.     final int msgLen = 4 //TOTALSIZE
  3.         + 4 //MAGICCODE  
  4.         + 4 //BODYCRC
  5.         + 4 //QUEUEID
  6.         + 4 //FLAG
  7.         + 8 //QUEUEOFFSET
  8.         + 8 //PHYSICALOFFSET
  9.         + 4 //SYSFLAG
  10.         + 8 //BORNTIMESTAMP
  11.         + 8 //BORNHOST
  12.         + 8 //STORETIMESTAMP
  13.         + 8 //STOREHOSTADDRESS
  14.         + 4 //RECONSUMETIMES
  15.         + 8 //Prepared Transaction Offset
  16.         + 4 + (bodyLength > 0 ? bodyLength : 0) //BODY
  17.         + 1 + topicLength //TOPIC
  18.         + 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength
  19.         + 0;
  20.     return msgLen;
  21. }
复制代码
代码:CommitLog#doAppend
  1. //消息长度不能超过4M
  2. if (msgLen > this.maxMessageSize) {
  3.     CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
  4.         + ", maxMessageSize: " + this.maxMessageSize);
  5.     return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED);
  6. }
  7. //消息是如果没有足够的存储空间则新创建CommitLog文件
  8. if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {
  9.     this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
  10.     // 1 TOTALSIZE
  11.     this.msgStoreItemMemory.putInt(maxBlank);
  12.     // 2 MAGICCODE
  13.     this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);
  14.     // 3 The remaining space may be any value
  15.     // Here the length of the specially set maxBlank
  16.     final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
  17.     byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
  18.     return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
  19.         queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
  20. }
  21. //将消息存储到ByteBuffer中,返回AppendMessageResult
  22. final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
  23. // Write messages to the queue buffer
  24. byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
  25. AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset,
  26.                                                      msgLen, msgId,msgInner.getStoreTimestamp(),
  27.                                                      queueOffset,
  28.                                                      CommitLog.this.defaultMessageStore.now()
  29.                                                      -beginTimeMills);
  30. switch (tranType) {
  31.     case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
  32.     case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
  33.         break;
  34.     case MessageSysFlag.TRANSACTION_NOT_TYPE:
  35.     case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
  36.         //更新消息队列偏移量
  37.         CommitLog.this.topicQueueTable.put(key, ++queueOffset);
  38.         break;
  39.     default:
  40.         break;
  41. }
复制代码
代码:CommitLog#putMessage
  1. //释放锁
  2. putMessageLock.unlock();
  3. //刷盘
  4. handleDiskFlush(result, putMessageResult, msg);
  5. //执行HA主从同步
  6. handleHA(result, putMessageResult, msg);
复制代码
存储文件

3.png


  • commitLog:消息存储目录
  • config:运行期间一些配置信息
  • consumerqueue:消息消费队列存储目录
  • index:消息索引文件存储目录
  • abort:如果存在改文件寿命Broker非正常关闭
  • checkpoint:文件检查点,存储CommitLog文件最后一次刷盘时间戳、consumerquueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。
存储文件内存映射

RocketMQ通过使用内存映射文件提高IO访问性能,无论是CommitLog、ConsumerQueue还是IndexFile,单个文件都被设计为固定长度,如果一个文件写满以后再创建一个新文件,文件名就为该文件第一条消息对应的全局物理偏移量。
1)MappedFileQueue

4.png
  1. String storePath;        //存储目录
  2. int mappedFileSize;        // 单个文件大小
  3. CopyOnWriteArrayList<MappedFile> mappedFiles;        //MappedFile文件集合
  4. AllocateMappedFileService allocateMappedFileService;        //创建MapFile服务类
  5. long flushedWhere = 0;                //当前刷盘指针
  6. long committedWhere = 0;        //当前数据提交指针,内存中ByteBuffer当前的写指针,该值大于等于flushWhere
复制代码

  • 根据存储时间查询MappedFile
  1. public MappedFile getMappedFileByTime(final long timestamp) {
  2.     Object[] mfs = this.copyMappedFiles(0);
  3.        
  4.     if (null == mfs)
  5.         return null;
  6.         //遍历MappedFile文件数组
  7.     for (int i = 0; i < mfs.length; i++) {
  8.         MappedFile mappedFile = (MappedFile) mfs[i];
  9.         //MappedFile文件的最后修改时间大于指定时间戳则返回该文件
  10.         if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
  11.             return mappedFile;
  12.         }
  13.     }
  14.     return (MappedFile) mfs[mfs.length - 1];
  15. }
复制代码

  • 根据消息偏移量offset查找MappedFile
  1. public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
  2.     try {
  3.         //获得第一个MappedFile文件
  4.         MappedFile firstMappedFile = this.getFirstMappedFile();
  5.         //获得最后一个MappedFile文件
  6.         MappedFile lastMappedFile = this.getLastMappedFile();
  7.         //第一个文件和最后一个文件均不为空,则进行处理
  8.         if (firstMappedFile != null && lastMappedFile != null) {
  9.             if (offset < firstMappedFile.getFileFromOffset() ||
  10.                 offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) {
  11.             } else {
  12.                 //获得文件索引
  13.                 int index = (int) ((offset / this.mappedFileSize)
  14.                                    - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
  15.                 MappedFile targetFile = null;
  16.                 try {
  17.                     //根据索引返回目标文件
  18.                     targetFile = this.mappedFiles.get(index);
  19.                 } catch (Exception ignored) {
  20.                 }
  21.                 if (targetFile != null && offset >= targetFile.getFileFromOffset()
  22.                     && offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
  23.                     return targetFile;
  24.                 }
  25.                 for (MappedFile tmpMappedFile : this.mappedFiles) {
  26.                     if (offset >= tmpMappedFile.getFileFromOffset()
  27.                         && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
  28.                         return tmpMappedFile;
  29.                     }
  30.                 }
  31.             }
  32.             if (returnFirstOnNotFound) {
  33.                 return firstMappedFile;
  34.             }
  35.         }
  36.     } catch (Exception e) {
  37.         log.error("findMappedFileByOffset Exception", e);
  38.     }
  39.     return null;
  40. }
复制代码

  • 获取存储文件最小偏移量
  1. public long getMinOffset() {
  2.     if (!this.mappedFiles.isEmpty()) {
  3.         try {
  4.             return this.mappedFiles.get(0).getFileFromOffset();
  5.         } catch (IndexOutOfBoundsException e) {
  6.             //continue;
  7.         } catch (Exception e) {
  8.             log.error("getMinOffset has exception.", e);
  9.         }
  10.     }
  11.     return -1;
  12. }
复制代码

  • 获取存储文件最大偏移量
  1. public long getMaxOffset() {
  2.     MappedFile mappedFile = getLastMappedFile();
  3.     if (mappedFile != null) {
  4.         return mappedFile.getFileFromOffset() + mappedFile.getReadPosition();
  5.     }
  6.     return 0;
  7. }
复制代码

  • 返回存储文件当前写指针
  1. public long getMaxWrotePosition() {
  2.     MappedFile mappedFile = getLastMappedFile();
  3.     if (mappedFile != null) {
  4.         return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition();
  5.     }
  6.     return 0;
  7. }
复制代码
2)MappedFile

5.png
  1. int OS_PAGE_SIZE = 1024 * 4;                //操作系统每页大小,默认4K
  2. AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);        //当前JVM实例中MappedFile虚拟内存
  3. AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);        //当前JVM实例中MappedFile对象个数
  4. AtomicInteger wrotePosition = new AtomicInteger(0);        //当前文件的写指针
  5. AtomicInteger committedPosition = new AtomicInteger(0);        //当前文件的提交指针
  6. AtomicInteger flushedPosition = new AtomicInteger(0);        //刷写到磁盘指针
  7. int fileSize;        //文件大小
  8. FileChannel fileChannel;        //文件通道       
  9. ByteBuffer writeBuffer = null;        //堆外内存ByteBuffer
  10. TransientStorePool transientStorePool = null;        //堆外内存池
  11. String fileName;        //文件名称
  12. long fileFromOffset;        //该文件的处理偏移量
  13. File file;        //物理文件
  14. MappedByteBuffer mappedByteBuffer;        //物理文件对应的内存映射Buffer
  15. volatile long storeTimestamp = 0;        //文件最后一次内容写入时间
  16. boolean firstCreateInQueue = false;        //是否是MappedFileQueue队列中第一个文件
复制代码
MappedFile初始化

  • 未开启transientStorePoolEnable。transientStorePoolEnable=true为true表示数据先存储到堆外内存,然后通过Commit线程将数据提交到内存映射Buffer中,再通过Flush线程将内存映射Buffer中数据持久化磁盘。
  1. private void init(final String fileName, final int fileSize) throws IOException {
  2.     this.fileName = fileName;
  3.     this.fileSize = fileSize;
  4.     this.file = new File(fileName);
  5.     this.fileFromOffset = Long.parseLong(this.file.getName());
  6.     boolean ok = false;
  7.        
  8.     ensureDirOK(this.file.getParent());
  9.     try {
  10.         this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
  11.         this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
  12.         TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
  13.         TOTAL_MAPPED_FILES.incrementAndGet();
  14.         ok = true;
  15.     } catch (FileNotFoundException e) {
  16.         log.error("create file channel " + this.fileName + " Failed. ", e);
  17.         throw e;
  18.     } catch (IOException e) {
  19.         log.error("map file " + this.fileName + " Failed. ", e);
  20.         throw e;
  21.     } finally {
  22.         if (!ok && this.fileChannel != null) {
  23.             this.fileChannel.close();
  24.         }
  25.     }
  26. }
复制代码
开启transientStorePoolEnable
  1. public void init(final String fileName, final int fileSize,
  2.     final TransientStorePool transientStorePool) throws IOException {
  3.     init(fileName, fileSize);
  4.     this.writeBuffer = transientStorePool.borrowBuffer();        //初始化writeBuffer
  5.     this.transientStorePool = transientStorePool;
  6. }
复制代码
MappedFile提交
提交数据到FileChannel,commitLeastPages为本次提交最小的页数,如果待提交数据不满commitLeastPages,则不执行本次提交操作。如果writeBuffer如果为空,直接返回writePosition指针,无需执行commit操作,表名commit操作主体是writeBuffer。
  1. public int commit(final int commitLeastPages) {
  2.     if (writeBuffer == null) {
  3.         //no need to commit data to file channel, so just regard wrotePosition as committedPosition.
  4.         return this.wrotePosition.get();
  5.     }
  6.     //判断是否满足提交条件
  7.     if (this.isAbleToCommit(commitLeastPages)) {
  8.         if (this.hold()) {
  9.             commit0(commitLeastPages);
  10.             this.release();
  11.         } else {
  12.             log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
  13.         }
  14.     }
  15.     // 所有数据提交后,清空缓冲区
  16.     if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
  17.         this.transientStorePool.returnBuffer(writeBuffer);
  18.         this.writeBuffer = null;
  19.     }
  20.     return this.committedPosition.get();
  21. }
复制代码
MappedFile#isAbleToCommit
判断是否执行commit操作,如果文件已满返回true;如果commitLeastpages大于0,则比较writePosition与上一次提交的指针commitPosition的差值,除以OS_PAGE_SIZE得到当前脏页的数量,如果大于commitLeastPages则返回true,如果commitLeastpages小于0表示只要存在脏页就提交。
  1. protected boolean isAbleToCommit(final int commitLeastPages) {
  2.     //已经刷盘指针
  3.     int flush = this.committedPosition.get();
  4.     //文件写指针
  5.     int write = this.wrotePosition.get();
  6.         //写满刷盘
  7.     if (this.isFull()) {
  8.         return true;
  9.     }
  10.     if (commitLeastPages > 0) {
  11.         //文件内容达到commitLeastPages页数,则刷盘
  12.         return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
  13.     }
  14.     return write > flush;
  15. }
复制代码
MappedFile#commit0
具体提交的实现,首先创建WriteBuffer区共享缓存区,然后将新创建的position回退到上一次提交的位置(commitPosition),设置limit为wrotePosition(当前最大有效数据指针),然后把commitPosition到wrotePosition的数据写入到FileChannel中,然后更新committedPosition指针为wrotePosition。commit的作用就是将MappedFile的writeBuffer中数据提交到文件通道FileChannel中。
  1. protected void commit0(final int commitLeastPages) {
  2.     //写指针
  3.     int writePos = this.wrotePosition.get();
  4.     //上次提交指针
  5.     int lastCommittedPosition = this.committedPosition.get();
  6.     if (writePos - this.committedPosition.get() > 0) {
  7.         try {
  8.             //复制共享内存区域
  9.             ByteBuffer byteBuffer = writeBuffer.slice();
  10.             //设置提交位置是上次提交位置
  11.             byteBuffer.position(lastCommittedPosition);
  12.             //最大提交数量
  13.             byteBuffer.limit(writePos);
  14.             //设置fileChannel位置为上次提交位置
  15.             this.fileChannel.position(lastCommittedPosition);
  16.             //将lastCommittedPosition到writePos的数据复制到FileChannel中
  17.             this.fileChannel.write(byteBuffer);
  18.             //重置提交位置
  19.             this.committedPosition.set(writePos);
  20.         } catch (Throwable e) {
  21.             log.error("Error occurred when commit data to FileChannel.", e);
  22.         }
  23.     }
  24. }
复制代码
MappedFile#flush
刷写磁盘,直接调用MappedByteBuffer或fileChannel的force方法将内存中的数据持久化到磁盘,那么flushedPosition应该等于MappedByteBuffer中的写指针;如果writeBuffer不为空,则flushPosition应该等于上一次的commit指针;因为上一次提交的数据就是进入到MappedByteBuffer中的数据;如果writeBuffer为空,数据时直接进入到MappedByteBuffer,wrotePosition代表的是MappedByteBuffer中的指针,故设置flushPosition为wrotePosition。
6.jpeg
  1. public int flush(final int flushLeastPages) {
  2.     //数据达到刷盘条件
  3.     if (this.isAbleToFlush(flushLeastPages)) {
  4.         //加锁,同步刷盘
  5.         if (this.hold()) {
  6.             //获得读指针
  7.             int value = getReadPosition();
  8.             try {
  9.                 //数据从writeBuffer提交数据到fileChannel再刷新到磁盘
  10.                 if (writeBuffer != null || this.fileChannel.position() != 0) {
  11.                     this.fileChannel.force(false);
  12.                 } else {
  13.                     //从mmap刷新数据到磁盘
  14.                     this.mappedByteBuffer.force();
  15.                 }
  16.             } catch (Throwable e) {
  17.                 log.error("Error occurred when force data to disk.", e);
  18.             }
  19.                         //更新刷盘位置
  20.             this.flushedPosition.set(value);
  21.             this.release();
  22.         } else {
  23.             log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
  24.             this.flushedPosition.set(getReadPosition());
  25.         }
  26.     }
  27.     return this.getFlushedPosition();
  28. }
复制代码
MappedFile#getReadPosition
获取当前文件最大可读指针。如果writeBuffer为空,则直接返回当前的写指针;如果writeBuffer不为空,则返回上一次提交的指针。在MappedFile设置中,只有提交了的数据(写入到MappedByteBuffer或FileChannel中的数据)才是安全的数据
  1. public int getReadPosition() {
  2.     //如果writeBuffer为空,刷盘的位置就是应该等于上次commit的位置,如果为空则为mmap的写指针
  3.     return this.writeBuffer == null ? this.wrotePosition.get() : this.committedPosition.get();
  4. }
复制代码
MappedFile#selectMappedBuffer
查找pos到当前最大可读之间的数据,由于在整个写入期间都未曾改MappedByteBuffer的指针,如果mappedByteBuffer.slice()方法返回的共享缓存区空间为整个MappedFile,然后通过设置ByteBuffer的position为待查找的值,读取字节长度当前可读最大长度,最终返回的ByteBuffer的limit为size。整个共享缓存区的容量为(MappedFile#fileSize-pos)。故在操作SelectMappedBufferResult不能对包含在里面的ByteBuffer调用filp方法。
  1. public SelectMappedBufferResult selectMappedBuffer(int pos) {
  2.     //获得最大可读指针
  3.     int readPosition = getReadPosition();
  4.     //pos小于最大可读指针,并且大于0
  5.     if (pos < readPosition && pos >= 0) {
  6.         if (this.hold()) {
  7.             //复制mappedByteBuffer读共享区
  8.             ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
  9.             //设置读指针位置
  10.             byteBuffer.position(pos);
  11.             //获得可读范围
  12.             int size = readPosition - pos;
  13.             //设置最大刻度范围
  14.             ByteBuffer byteBufferNew = byteBuffer.slice();
  15.             byteBufferNew.limit(size);
  16.             return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
  17.         }
  18.     }
  19.     return null;
  20. }
复制代码
MappedFile#shutdown
MappedFile文件销毁的实现方法为public boolean destory(long intervalForcibly),intervalForcibly表示拒绝被销毁的最大存活时间。
  1. public void shutdown(final long intervalForcibly) {
  2.     if (this.available) {
  3.         //关闭MapedFile
  4.         this.available = false;
  5.         //设置当前关闭时间戳
  6.         this.firstShutdownTimestamp = System.currentTimeMillis();
  7.         //释放资源
  8.         this.release();
  9.     } else if (this.getRefCount() > 0) {
  10.         if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
  11.             this.refCount.set(-1000 - this.getRefCount());
  12.             this.release();
  13.         }
  14.     }
  15. }
复制代码
3)TransientStorePool

短暂的存储池。RocketMQ单独创建一个MappedByteBuffer内存缓存池,用来临时存储数据,数据先写入该内存映射中,然后由commit线程定时将数据从该内存复制到与目标物理文件对应的内存映射中。RocketMQ引入该机制主要的原因是提供一种内存锁定,将当前堆外内存一直锁定在内存中,避免被进程将内存交换到磁盘。
7.png
  1. private final int poolSize;                //availableBuffers个数
  2. private final int fileSize;                //每隔ByteBuffer大小
  3. private final Deque<ByteBuffer> availableBuffers;        //ByteBuffer容器。双端队列
复制代码
初始化
  1. public void init() {
  2.     //创建poolSize个堆外内存
  3.     for (int i = 0; i < poolSize; i++) {
  4.         ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);
  5.         final long address = ((DirectBuffer) byteBuffer).address();
  6.         Pointer pointer = new Pointer(address);
  7.         //使用com.sun.jna.Library类库将该批内存锁定,避免被置换到交换区,提高存储性能
  8.         LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));
  9.         availableBuffers.offer(byteBuffer);
  10.     }
  11. }
复制代码
实时更新消息消费队列与索引文件

消息消费队文件、消息属性索引文件都是基于CommitLog文件构建的,当消息生产者提交的消息存储在CommitLog文件中,ConsumerQueue、IndexFile需要及时更新,否则消息无法及时被消费,根据消息属性查找消息也会出现较大延迟。RocketMQ通过开启一个线程ReputMessageService来准实时转发CommitLog文件更新事件,相应的任务处理器根据转发的消息及时更新ConsumerQueue、IndexFile文件。
8.png

9.png

代码:DefaultMessageStore:start
  1. //设置CommitLog内存中最大偏移量
  2. this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
  3. //启动
  4. this.reputMessageService.start();
复制代码
代码:DefaultMessageStore:run
  1. public void run() {
  2.     DefaultMessageStore.log.info(this.getServiceName() + " service started");
  3.         //每隔1毫秒就继续尝试推送消息到消息消费队列和索引文件
  4.     while (!this.isStopped()) {
  5.         try {
  6.             Thread.sleep(1);
  7.             this.doReput();
  8.         } catch (Exception e) {
  9.             DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
  10.         }
  11.     }
  12.     DefaultMessageStore.log.info(this.getServiceName() + " service end");
  13. }
复制代码
代码:DefaultMessageStore:deReput
  1. //从result中循环遍历消息,一次读一条,创建DispatherRequest对象。
  2. for (int readSize = 0; readSize < result.getSize() && doNext; ) {
  3.         DispatchRequest dispatchRequest =                               DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
  4.         int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
  5.         if (dispatchRequest.isSuccess()) {
  6.             if (size > 0) {
  7.                 DefaultMessageStore.this.doDispatch(dispatchRequest);
  8.             }
  9.     }
  10. }
复制代码
DispatchRequest
10.png
  1. String topic; //消息主题名称
  2. int queueId;  //消息队列ID
  3. long commitLogOffset;        //消息物理偏移量
  4. int msgSize;        //消息长度
  5. long tagsCode;        //消息过滤tag hashCode
  6. long storeTimestamp;        //消息存储时间戳
  7. long consumeQueueOffset;        //消息队列偏移量
  8. String keys;        //消息索引key
  9. boolean success;        //是否成功解析到完整的消息
  10. String uniqKey;        //消息唯一键
  11. int sysFlag;        //消息系统标记
  12. long preparedTransactionOffset;        //消息预处理事务偏移量
  13. Map<String, String> propertiesMap;        //消息属性
  14. byte[] bitMap;        //位图
复制代码
1)转发到ConsumerQueue

11.png
  1. class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
  2.     @Override
  3.     public void dispatch(DispatchRequest request) {
  4.         final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
  5.         switch (tranType) {
  6.             case MessageSysFlag.TRANSACTION_NOT_TYPE:
  7.             case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
  8.                 //消息分发
  9.                 DefaultMessageStore.this.putMessagePositionInfo(request);
  10.                 break;
  11.             case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
  12.             case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
  13.                 break;
  14.         }
  15.     }
  16. }
复制代码
代码:DefaultMessageStore#putMessagePositionInfo
  1. public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
  2.     //获得消费队列
  3.     ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
  4.     //消费队列分发消息
  5.     cq.putMessagePositionInfoWrapper(dispatchRequest);
  6. }
复制代码
代码:DefaultMessageStore#putMessagePositionInfo
  1. //依次将消息偏移量、消息长度、tag写入到ByteBuffer中
  2. this.byteBufferIndex.flip();
  3. this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
  4. this.byteBufferIndex.putLong(offset);
  5. this.byteBufferIndex.putInt(size);
  6. this.byteBufferIndex.putLong(tagsCode);
  7. //获得内存映射文件
  8. MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
  9. if (mappedFile != null) {
  10.     //将消息追加到内存映射文件,异步输盘
  11.     return mappedFile.appendMessage(this.byteBufferIndex.array());
  12. }
复制代码
2)转发到Index

12.png
  1. class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
  2.     @Override
  3.     public void dispatch(DispatchRequest request) {
  4.         if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
  5.             DefaultMessageStore.this.indexService.buildIndex(request);
  6.         }
  7.     }
  8. }
复制代码
代码:DefaultMessageStore#buildIndex
  1. public void buildIndex(DispatchRequest req) {
  2.     //获得索引文件
  3.     IndexFile indexFile = retryGetAndCreateIndexFile();
  4.     if (indexFile != null) {
  5.         //获得文件最大物理偏移量
  6.         long endPhyOffset = indexFile.getEndPhyOffset();
  7.         DispatchRequest msg = req;
  8.         String topic = msg.getTopic();
  9.         String keys = msg.getKeys();
  10.         //如果该消息的物理偏移量小于索引文件中的最大物理偏移量,则说明是重复数据,忽略本次索引构建
  11.         if (msg.getCommitLogOffset() < endPhyOffset) {
  12.             return;
  13.         }
  14.         final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
  15.         switch (tranType) {
  16.             case MessageSysFlag.TRANSACTION_NOT_TYPE:
  17.             case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
  18.             case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
  19.                 break;
  20.             case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
  21.                 return;
  22.         }
  23.                
  24.         //如果消息ID不为空,则添加到Hash索引中
  25.         if (req.getUniqKey() != null) {
  26.             indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
  27.             if (indexFile == null) {
  28.                 return;
  29.             }
  30.         }
  31.                 //构建索引key,RocketMQ支持为同一个消息建立多个索引,多个索引键空格隔开.
  32.         if (keys != null && keys.length() > 0) {
  33.             String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
  34.             for (int i = 0; i < keyset.length; i++) {
  35.                 String key = keyset[i];
  36.                 if (key.length() > 0) {
  37.                     indexFile = putKey(indexFile, msg, buildKey(topic, key));
  38.                     if (indexFile == null) {
  39.                         return;
  40.                     }
  41.                 }
  42.             }
  43.         }
  44.     } else {
  45.         log.error("build index error, stop building index");
  46.     }
  47. }
复制代码
消息队列和索引文件恢复

由于RocketMQ存储首先将消息全量存储在CommitLog文件中,然后异步生成转发任务更新ConsumerQueue和Index文件。如果消息成功存储到CommitLog文件中,转发任务未成功执行,此时消息服务器Broker由于某个愿意宕机,导致CommitLog、ConsumerQueue、IndexFile文件数据不一致。如果不加以人工修复的话,会有一部分消息即便在CommitLog中文件中存在,但由于没有转发到ConsumerQueue,这部分消息将永远复发被消费者消费。
13.png

1)存储文件加载

代码:DefaultMessageStore#load
判断上一次是否异常退出。实现机制是Broker在启动时创建abort文件,在退出时通过JVM钩子函数删除abort文件。如果下次启动时存在abort文件。说明Broker时异常退出的,CommitLog与ConsumerQueue数据有可能不一致,需要进行修复。
  1. //判断临时文件是否存在
  2. boolean lastExitOK = !this.isTempFileExist();
  3. //根据临时文件判断当前Broker是否异常退出
  4. private boolean isTempFileExist() {
  5.     String fileName = StorePathConfigHelper
  6.         .getAbortFile(this.messageStoreConfig.getStorePathRootDir());
  7.     File file = new File(fileName);
  8.     return file.exists();
  9. }
复制代码
代码:DefaultMessageStore#load
  1. //加载延时队列
  2. if (null != scheduleMessageService) {
  3.     result = result && this.scheduleMessageService.load();
  4. }
  5. // 加载CommitLog文件
  6. result = result && this.commitLog.load();
  7. // 加载消费队列文件
  8. result = result && this.loadConsumeQueue();
  9. if (result) {
  10.         //加载存储监测点,监测点主要记录CommitLog文件、ConsumerQueue文件、Index索引文件的刷盘点
  11.     this.storeCheckpoint =new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir()));
  12.         //加载index文件
  13.     this.indexService.load(lastExitOK);
  14.         //根据Broker是否异常退出,执行不同的恢复策略
  15.     this.recover(lastExitOK);
  16. }
复制代码
代码:MappedFileQueue#load
加载CommitLog到映射文件
  1. //指向CommitLog文件目录
  2. File dir = new File(this.storePath);
  3. //获得文件数组
  4. File[] files = dir.listFiles();
  5. if (files != null) {
  6.     // 文件排序
  7.     Arrays.sort(files);
  8.     //遍历文件
  9.     for (File file : files) {
  10.                 //如果文件大小和配置文件不一致,退出
  11.         if (file.length() != this.mappedFileSize) {
  12.             
  13.             return false;
  14.         }
  15.         try {
  16.             //创建映射文件
  17.             MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);
  18.             mappedFile.setWrotePosition(this.mappedFileSize);
  19.             mappedFile.setFlushedPosition(this.mappedFileSize);
  20.             mappedFile.setCommittedPosition(this.mappedFileSize);
  21.             //将映射文件添加到队列
  22.             this.mappedFiles.add(mappedFile);
  23.             log.info("load " + file.getPath() + " OK");
  24.         } catch (IOException e) {
  25.             log.error("load file " + file + " error", e);
  26.             return false;
  27.         }
  28.     }
  29. }
  30. return true;
复制代码
代码:DefaultMessageStore#loadConsumeQueue
加载消息消费队列
  1. //执行消费队列目录
  2. File dirLogic = new File(StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()));
  3. //遍历消费队列目录
  4. File[] fileTopicList = dirLogic.listFiles();
  5. if (fileTopicList != null) {
  6.     for (File fileTopic : fileTopicList) {
  7.         //获得子目录名称,即topic名称
  8.         String topic = fileTopic.getName();
  9.                 //遍历子目录下的消费队列文件
  10.         File[] fileQueueIdList = fileTopic.listFiles();
  11.         if (fileQueueIdList != null) {
  12.             //遍历文件
  13.             for (File fileQueueId : fileQueueIdList) {
  14.                 //文件名称即队列ID
  15.                 int queueId;
  16.                 try {
  17.                     queueId = Integer.parseInt(fileQueueId.getName());
  18.                 } catch (NumberFormatException e) {
  19.                     continue;
  20.                 }
  21.                 //创建消费队列并加载到内存
  22.                 ConsumeQueue logic = new ConsumeQueue(
  23.                     topic,
  24.                     queueId,
  25.                     StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()),
  26.             this.getMessageStoreConfig().getMapedFileSizeConsumeQueue(),
  27.                     this);
  28.                 this.putConsumeQueue(topic, queueId, logic);
  29.                 if (!logic.load()) {
  30.                     return false;
  31.                 }
  32.             }
  33.         }
  34.     }
  35. }
  36. log.info("load logics queue all over, OK");
  37. return true;
复制代码
代码:IndexService#load
加载索引文件
  1. public boolean load(final boolean lastExitOK) {
  2.     //索引文件目录
  3.     File dir = new File(this.storePath);
  4.     //遍历索引文件
  5.     File[] files = dir.listFiles();
  6.     if (files != null) {
  7.         //文件排序
  8.         Arrays.sort(files);
  9.         //遍历文件
  10.         for (File file : files) {
  11.             try {
  12.                 //加载索引文件
  13.                 IndexFile f = new IndexFile(file.getPath(), this.hashSlotNum, this.indexNum, 0, 0);
  14.                 f.load();
  15.                 if (!lastExitOK) {
  16.                     //索引文件上次的刷盘时间小于该索引文件的消息时间戳,该文件将立即删除
  17.                     if (f.getEndTimestamp() > this.defaultMessageStore.getStoreCheckpoint()
  18.                         .getIndexMsgTimestamp()) {
  19.                         f.destroy(0);
  20.                         continue;
  21.                     }
  22.                 }
  23.                                 //将索引文件添加到队列
  24.                 log.info("load index file OK, " + f.getFileName());
  25.                 this.indexFileList.add(f);
  26.             } catch (IOException e) {
  27.                 log.error("load file {} error", file, e);
  28.                 return false;
  29.             } catch (NumberFormatException e) {
  30.                 log.error("load file {} error", file, e);
  31.             }
  32.         }
  33.     }
  34.     return true;
  35. }
复制代码
代码:DefaultMessageStore#recover
文件恢复,根据Broker是否正常退出执行不同的恢复策略
  1. private void recover(final boolean lastExitOK) {
  2.     //获得最大的物理便宜消费队列
  3.     long maxPhyOffsetOfConsumeQueue = this.recoverConsumeQueue();
  4.     if (lastExitOK) {
  5.         //正常恢复
  6.         this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);
  7.     } else {
  8.         //异常恢复
  9.         this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);
  10.     }
  11.         //在CommitLog中保存每个消息消费队列当前的存储逻辑偏移量
  12.     this.recoverTopicQueueTable();
  13. }
复制代码
代码:DefaultMessageStore#recoverTopicQueueTable
恢复ConsumerQueue后,将在CommitLog实例中保存每隔消息队列当前的存储逻辑偏移量,这也是消息中不仅存储主题、消息队列ID、还存储了消息队列的关键所在。
  1. public void recoverTopicQueueTable() {
  2.     HashMap<String/* topic-queueid */, Long/* offset */> table = new HashMap<String, Long>(1024);
  3.     //CommitLog最小偏移量
  4.     long minPhyOffset = this.commitLog.getMinOffset();
  5.     //遍历消费队列,将消费队列保存在CommitLog中
  6.     for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
  7.         for (ConsumeQueue logic : maps.values()) {
  8.             String key = logic.getTopic() + "-" + logic.getQueueId();
  9.             table.put(key, logic.getMaxOffsetInQueue());
  10.             logic.correctMinOffset(minPhyOffset);
  11.         }
  12.     }
  13.     this.commitLog.setTopicQueueTable(table);
  14. }
复制代码
2)正常恢复

代码:CommitLog#recoverNormally
  1. public void recoverNormally(long maxPhyOffsetOfConsumeQueue) {
  2.        
  3.     final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
  4.     if (!mappedFiles.isEmpty()) {
  5.          //Broker正常停止再重启时,从倒数第三个开始恢复,如果不足3个文件,则从第一个文件开始恢复。
  6.         int index = mappedFiles.size() - 3;
  7.         if (index < 0)
  8.             index = 0;
  9.         MappedFile mappedFile = mappedFiles.get(index);
  10.         ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
  11.         long processOffset = mappedFile.getFileFromOffset();
  12.         //代表当前已校验通过的offset
  13.         long mappedFileOffset = 0;
  14.         while (true) {
  15.             //查找消息
  16.             DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover);
  17.             //消息长度
  18.             int size = dispatchRequest.getMsgSize();
  19.                    //查找结果为true,并且消息长度大于0,表示消息正确.mappedFileOffset向前移动本消息长度
  20.             if (dispatchRequest.isSuccess() && size > 0) {
  21.                 mappedFileOffset += size;
  22.             }
  23.                         //如果查找结果为true且消息长度等于0,表示已到该文件末尾,如果还有下一个文件,则重置processOffset和MappedFileOffset重复查找下一个文件,否则跳出循环。
  24.             else if (dispatchRequest.isSuccess() && size == 0) {
  25.               index++;
  26.               if (index >= mappedFiles.size()) {
  27.                   // Current branch can not happen
  28.                   break;
  29.               } else {
  30.                   //取出每个文件
  31.                   mappedFile = mappedFiles.get(index);
  32.                   byteBuffer = mappedFile.sliceByteBuffer();
  33.                   processOffset = mappedFile.getFileFromOffset();
  34.                   mappedFileOffset = 0;
  35.                   
  36.                           }
  37.             }
  38.             // 查找结果为false,表明该文件未填满所有消息,跳出循环,结束循环
  39.             else if (!dispatchRequest.isSuccess()) {
  40.                 log.info("recover physics file end, " + mappedFile.getFileName());
  41.                 break;
  42.             }
  43.         }
  44.                 //更新MappedFileQueue的flushedWhere和committedWhere指针
  45.         processOffset += mappedFileOffset;
  46.         this.mappedFileQueue.setFlushedWhere(processOffset);
  47.         this.mappedFileQueue.setCommittedWhere(processOffset);
  48.         //删除offset之后的所有文件
  49.         this.mappedFileQueue.truncateDirtyFiles(processOffset);
  50.         
  51.         if (maxPhyOffsetOfConsumeQueue >= processOffset) {
  52.             this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
  53.         }
  54.     } else {
  55.         this.mappedFileQueue.setFlushedWhere(0);
  56.         this.mappedFileQueue.setCommittedWhere(0);
  57.         this.defaultMessageStore.destroyLogics();
  58.     }
  59. }
复制代码
代码:MappedFileQueue#truncateDirtyFiles
  1. public void truncateDirtyFiles(long offset) {
  2.     List<MappedFile> willRemoveFiles = new ArrayList<MappedFile>();
  3.         //遍历目录下文件
  4.     for (MappedFile file : this.mappedFiles) {
  5.         //文件尾部的偏移量
  6.         long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize;
  7.         //文件尾部的偏移量大于offset
  8.         if (fileTailOffset > offset) {
  9.             //offset大于文件的起始偏移量
  10.             if (offset >= file.getFileFromOffset()) {
  11.                 //更新wrotePosition、committedPosition、flushedPosistion
  12.                 file.setWrotePosition((int) (offset % this.mappedFileSize));
  13.                 file.setCommittedPosition((int) (offset % this.mappedFileSize));
  14.                 file.setFlushedPosition((int) (offset % this.mappedFileSize));
  15.             } else {
  16.                 //offset小于文件的起始偏移量,说明该文件是有效文件后面创建的,释放mappedFile占用内存,删除文件
  17.                 file.destroy(1000);
  18.                 willRemoveFiles.add(file);
  19.             }
  20.         }
  21.     }
  22.     this.deleteExpiredFile(willRemoveFiles);
  23. }
复制代码
3)异常恢复

Broker异常停止文件恢复的实现为CommitLog#recoverAbnormally。异常文件恢复步骤与正常停止文件恢复流程基本相同,其主要差别有两个。首先,正常停止默认从倒数第三个文件开始进行恢复,而异常停止则需要从最后一个文件往前走,找到第一个消息存储正常的文件。其次,如果CommitLog目录没有消息文件,如果消息消费队列目录下存在文件,则需要销毁。
代码:CommitLog#recoverAbnormally
  1. if (!mappedFiles.isEmpty()) {
  2.     // Looking beginning to recover from which file
  3.     int index = mappedFiles.size() - 1;
  4.     MappedFile mappedFile = null;
  5.     for (; index >= 0; index--) {
  6.         mappedFile = mappedFiles.get(index);
  7.         //判断消息文件是否是一个正确的文件
  8.         if (this.isMappedFileMatchedRecover(mappedFile)) {
  9.             log.info("recover from this mapped file " + mappedFile.getFileName());
  10.             break;
  11.         }
  12.     }
  13.         //根据索引取出mappedFile文件
  14.     if (index < 0) {
  15.         index = 0;
  16.         mappedFile = mappedFiles.get(index);
  17.     }
  18.     //...验证消息的合法性,并将消息转发到消息消费队列和索引文件
  19.       
  20. }else{
  21.     //未找到mappedFile,重置flushWhere、committedWhere都为0,销毁消息队列文件
  22.     this.mappedFileQueue.setFlushedWhere(0);
  23.     this.mappedFileQueue.setCommittedWhere(0);
  24.     this.defaultMessageStore.destroyLogics();
  25. }
复制代码
刷盘机制

RocketMQ的存储是基于JDK NIO的内存映射机制(MappedByteBuffer)的,消息存储首先将消息追加到内存,再根据配置的刷盘策略在不同时间进行刷写磁盘。
同步刷盘

消息追加到内存后,立即将数据刷写到磁盘文件
14.png

代码:CommitLog#handleDiskFlush
  1. //刷盘服务
  2. final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
  3. if (messageExt.isWaitStoreMsgOK()) {
  4.     //封装刷盘请求
  5.     GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
  6.     //提交刷盘请求
  7.     service.putRequest(request);
  8.     //线程阻塞5秒,等待刷盘结束
  9.     boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
  10.     if (!flushOK) {
  11.         putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
  12.     }
复制代码
GroupCommitRequest
15.png
  1. long nextOffset;        //刷盘点偏移量
  2. CountDownLatch countDownLatch = new CountDownLatch(1);        //倒计树锁存器
  3. volatile boolean flushOK = false;        //刷盘结果;默认为false
复制代码
代码:GroupCommitService#run
  1. public void run() {
  2.     CommitLog.log.info(this.getServiceName() + " service started");
  3.     while (!this.isStopped()) {
  4.         try {
  5.             //线程等待10ms
  6.             this.waitForRunning(10);
  7.             //执行提交
  8.             this.doCommit();
  9.         } catch (Exception e) {
  10.             CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
  11.         }
  12.     }
  13.         ...
  14. }
复制代码
代码:GroupCommitService#doCommit
  1. private void doCommit() {
  2.     //加锁
  3.     synchronized (this.requestsRead) {
  4.         if (!this.requestsRead.isEmpty()) {
  5.             //遍历requestsRead
  6.             for (GroupCommitRequest req : this.requestsRead) {
  7.                 // There may be a message in the next file, so a maximum of
  8.                 // two times the flush
  9.                 boolean flushOK = false;
  10.                 for (int i = 0; i < 2 && !flushOK; i++) {
  11.                     flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
  12.                                         //刷盘
  13.                     if (!flushOK) {
  14.                         CommitLog.this.mappedFileQueue.flush(0);
  15.                     }
  16.                 }
  17.                                 //唤醒发送消息客户端
  18.                 req.wakeupCustomer(flushOK);
  19.             }
  20.                        
  21.             //更新刷盘监测点
  22.             long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
  23.             if (storeTimestamp > 0) {               CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
  24.             }
  25.                        
  26.             this.requestsRead.clear();
  27.         } else {
  28.             // Because of individual messages is set to not sync flush, it
  29.             // will come to this process
  30.             CommitLog.this.mappedFileQueue.flush(0);
  31.         }
  32.     }
  33. }
复制代码
异步刷盘

在消息追加到内存后,立即返回给消息发送端。如果开启transientStorePoolEnable,RocketMQ会单独申请一个与目标物理文件(commitLog)同样大小的堆外内存,该堆外内存将使用内存锁定,确保不会被置换到虚拟内存中去,消息首先追加到堆外内存,然后提交到物理文件的内存映射中,然后刷写到磁盘。如果未开启transientStorePoolEnable,消息直接追加到物理文件直接映射文件中,然后刷写到磁盘中。
16.png

开启transientStorePoolEnable后异步刷盘步骤:

  • 将消息直接追加到ByteBuffer(堆外内存)
  • CommitRealTimeService线程每隔200ms将ByteBuffer新追加内容提交到MappedByteBuffer中
  • MappedByteBuffer在内存中追加提交的内容,wrotePosition指针向后移动
  • commit操作成功返回,将committedPosition位置恢复
  • FlushRealTimeService线程默认每500ms将MappedByteBuffer中新追加的内存刷写到磁盘
代码:CommitLog$CommitRealTimeService#run
提交线程工作机制
  1. //间隔时间,默认200ms
  2. int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
  3. //一次提交的至少页数
  4. int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();
  5. //两次真实提交的最大间隔,默认200ms
  6. int commitDataThoroughInterval =
  7. CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();
  8. //上次提交间隔超过commitDataThoroughInterval,则忽略提交commitDataThoroughInterval参数,直接提交
  9. long begin = System.currentTimeMillis();
  10. if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
  11.     this.lastCommitTimestamp = begin;
  12.     commitDataLeastPages = 0;
  13. }
  14. //执行提交操作,将待提交数据提交到物理文件的内存映射区
  15. boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
  16. long end = System.currentTimeMillis();
  17. if (!result) {
  18.     this.lastCommitTimestamp = end; // result = false means some data committed.
  19.     //now wake up flush thread.
  20.     //唤醒刷盘线程
  21.     flushCommitLogService.wakeup();
  22. }
  23. if (end - begin > 500) {
  24.     log.info("Commit data to file costs {} ms", end - begin);
  25. }
  26. this.waitForRunning(interval);
复制代码
代码:CommitLog$FlushRealTimeService#run
刷盘线程工作机制
  1. //表示await方法等待,默认false
  2. boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
  3. //线程执行时间间隔
  4. int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
  5. //一次刷写任务至少包含页数
  6. int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
  7. //两次真实刷写任务最大间隔
  8. int flushPhysicQueueThoroughInterval =
  9. CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();
  10. ...
  11. //距离上次提交间隔超过flushPhysicQueueThoroughInterval,则本次刷盘任务将忽略flushPhysicQueueLeastPages,直接提交
  12. long currentTimeMillis = System.currentTimeMillis();
  13. if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
  14.     this.lastFlushTimestamp = currentTimeMillis;
  15.     flushPhysicQueueLeastPages = 0;
  16.     printFlushProgress = (printTimes++ % 10) == 0;
  17. }
  18. ...
  19. //执行一次刷盘前,先等待指定时间间隔
  20. if (flushCommitLogTimed) {
  21.     Thread.sleep(interval);
  22. } else {
  23.     this.waitForRunning(interval);
  24. }
  25. ...
  26. long begin = System.currentTimeMillis();
  27. //刷写磁盘
  28. CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
  29. long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
  30. if (storeTimestamp > 0) {
  31. //更新存储监测点文件的时间戳
  32. CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
复制代码
过期文件删除机制

由于RocketMQ操作CommitLog、ConsumerQueue文件是基于内存映射机制并在启动的时候回加载CommitLog、ConsumerQueue目录下的所有文件,为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,所以要引入一种机制来删除已过期的文件。RocketMQ顺序写CommitLog、ConsumerQueue文件,所有写操作全部落在最后一个CommitLog或者ConsumerQueue文件上,之前的文件在下一个文件创建后将不会再被更新。RocketMQ清除过期文件的方法时:如果当前文件在在一定时间间隔内没有再次被消费,则认为是过期文件,可以被删除,RocketMQ不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为72小时,通过在Broker配置文件中设置fileReservedTime来改变过期时间,单位为小时。
代码:DefaultMessageStore#addScheduleTask
  1. private void addScheduleTask() {
  2.         //每隔10s调度一次清除文件
  3.     this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
  4.         @Override
  5.         public void run() {
  6.             DefaultMessageStore.this.cleanFilesPeriodically();
  7.         }
  8.     }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
  9.         ...
  10. }
复制代码
代码:DefaultMessageStore#cleanFilesPeriodically
  1. private void cleanFilesPeriodically() {
  2.     //清除存储文件
  3.     this.cleanCommitLogService.run();
  4.     //清除消息消费队列文件
  5.     this.cleanConsumeQueueService.run();
  6. }
复制代码
代码:DefaultMessageStore#deleteExpiredFiles
  1. private void deleteExpiredFiles() {
  2.     //删除的数量
  3.     int deleteCount = 0;
  4.     //文件保留的时间
  5.     long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
  6.     //删除物理文件的间隔
  7.     int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval();
  8.     //线程被占用,第一次拒绝删除后能保留的最大时间,超过该时间,文件将被强制删除
  9.     int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly();
  10. boolean timeup = this.isTimeToDelete();
  11. boolean spacefull = this.isSpaceToDelete();
  12. boolean manualDelete = this.manualDeleteFileSeveralTimes > 0;
  13. if (timeup || spacefull || manualDelete) {
  14.         ...执行删除逻辑
  15. }else{
  16.     ...无作为
  17. }
复制代码
删除文件操作的条件

  • 指定删除文件的时间点,RocketMQ通过deleteWhen设置一天的固定时间执行一次删除过期文件操作,默认4点
  • 磁盘空间如果不充足,删除过期文件
  • 预留,手工触发。
代码:CleanCommitLogService#isSpaceToDelete
当磁盘空间不足时执行删除过期文件
  1. private boolean isSpaceToDelete() {
  2.     //磁盘分区的最大使用量
  3.     double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0;
  4.         //是否需要立即执行删除过期文件操作
  5.     cleanImmediately = false;
  6.     {
  7.         String storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog();
  8.         //当前CommitLog目录所在的磁盘分区的磁盘使用率
  9.         double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic);
  10.         //diskSpaceWarningLevelRatio:磁盘使用率警告阈值,默认0.90
  11.         if (physicRatio > diskSpaceWarningLevelRatio) {
  12.             boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull();
  13.             if (diskok) {
  14.                 DefaultMessageStore.log.error("physic disk maybe full soon " + physicRatio + ", so mark disk full");
  15.             }
  16.                         //diskSpaceCleanForciblyRatio:强制清除阈值,默认0.85
  17.             cleanImmediately = true;
  18.         } else if (physicRatio > diskSpaceCleanForciblyRatio) {
  19.             cleanImmediately = true;
  20.         } else {
  21.             boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK();
  22.             if (!diskok) {
  23.             DefaultMessageStore.log.info("physic disk space OK " + physicRatio + ", so mark disk ok");
  24.         }
  25.     }
  26.     if (physicRatio < 0 || physicRatio > ratio) {
  27.         DefaultMessageStore.log.info("physic disk maybe full soon, so reclaim space, " + physicRatio);
  28.         return true;
  29.     }
  30. }
复制代码
代码:MappedFileQueue#deleteExpiredFileByTime
执行文件销毁和删除
  1. for (int i = 0; i < mfsLength; i++) {
  2.     //遍历每隔文件
  3.     MappedFile mappedFile = (MappedFile) mfs[i];
  4.     //计算文件存活时间
  5.     long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
  6.     //如果超过72小时,执行文件删除
  7.     if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
  8.         if (mappedFile.destroy(intervalForcibly)) {
  9.             files.add(mappedFile);
  10.             deleteCount++;
  11.             if (files.size() >= DELETE_FILES_BATCH_MAX) {
  12.                 break;
  13.             }
  14.             if (deleteFilesInterval > 0 && (i + 1) < mfsLength) {
  15.                 try {
  16.                     Thread.sleep(deleteFilesInterval);
  17.                 } catch (InterruptedException e) {
  18.                 }
  19.             }
  20.         } else {
  21.             break;
  22.         }
  23.     } else {
  24.         //avoid deleting files in the middle
  25.         break;
  26.     }
  27. }
复制代码
小结

RocketMQ的存储文件包括消息文件(Commitlog)、消息消费队列文件(ConsumerQueue)、Hash索引文件(IndexFile)、监测点文件(checkPoint)、abort(关闭异常文件)。单个消息存储文件、消息消费队列文件、Hash索引文件长度固定以便使用内存映射机制进行文件的读写操作。RocketMQ组织文件以文件的起始偏移量来命令文件,这样根据偏移量能快速定位到真实的物理文件。RocketMQ基于内存映射文件机制提供了同步刷盘和异步刷盘两种机制,异步刷盘是指在消息存储时先追加到内存映射文件,然后启动专门的刷盘线程定时将内存中的文件数据刷写到磁盘。
CommitLog,消息存储文件,RocketMQ为了保证消息发送的高吞吐量,采用单一文件存储所有主题消息,保证消息存储是完全的顺序写,但这样给文件读取带来了不便,为此RocketMQ为了方便消息消费构建了消息消费队列文件,基于主题与队列进行组织,同时RocketMQ为消息实现了Hash索引,可以为消息设置索引键,根据所以能够快速从CommitLog文件中检索消息。
当消息达到CommitLog后,会通过ReputMessageService线程接近实时地将消息转发给消息消费队列文件与索引文件。为了安全起见,RocketMQ引入abort文件,记录Broker的停机是否是正常关闭还是异常关闭,在重启Broker时为了保证CommitLog文件,消息消费队列文件与Hash索引文件的正确性,分别采用不同策略来恢复文件。
RocketMQ不会永久存储消息文件、消息消费队列文件,而是启动文件过期机制并在磁盘空间不足或者默认凌晨4点删除过期文件,文件保存72小时并且在删除文件时并不会判断该消息文件上的消息是否被消费。
Consumer

消息消费概述

消息消费以组的模式开展,一个消费组内可以包含多个消费者,每一个消费者组可订阅多个主题,消费组之间有ff式和广播模式两种消费模式。集群模式,主题下的同一条消息只允许被其中一个消费者消费。广播模式,主题下的同一条消息,将被集群内的所有消费者消费一次。消息服务器与消费者之间的消息传递也有两种模式:推模式、拉模式。所谓的拉模式,是消费端主动拉起拉消息请求,而推模式是消息达到消息服务器后,推送给消息消费者。RocketMQ消息推模式的实现基于拉模式,在拉模式上包装一层,一个拉取任务完成后开始下一个拉取任务。
集群模式下,多个消费者如何对消息队列进行负载呢?消息队列负载机制遵循一个通用思想:一个消息队列同一个时间只允许被一个消费者消费,一个消费者可以消费多个消息队列。
RocketMQ支持局部顺序消息消费,也就是保证同一个消息队列上的消息顺序消费。不支持消息全局顺序消费,如果要实现某一个主题的全局顺序消费,可以将该主题的队列数设置为1,牺牲高可用性。
消息消费初探

消息推送模式
17.png

消息消费重要方法
  1. void sendMessageBack(final MessageExt msg, final int delayLevel, final String brokerName):发送消息确认
  2. Set<MessageQueue> fetchSubscribeMessageQueues(final String topic) :获取消费者对主题分配了那些消息队列
  3. void registerMessageListener(final MessageListenerConcurrently messageListener):注册并发事件监听器
  4. void registerMessageListener(final MessageListenerOrderly messageListener):注册顺序消息事件监听器
  5. void subscribe(final String topic, final String subExpression):基于主题订阅消息,消息过滤使用表达式
  6. void subscribe(final String topic, final String fullClassName,final String filterClassSource):基于主题订阅消息,消息过滤使用类模式
  7. void subscribe(final String topic, final MessageSelector selector) :订阅消息,并指定队列选择器
  8. void unsubscribe(final String topic):取消消息订阅
复制代码
DefaultMQPushConsumer
18.png
  1. //消费者组
  2. private String consumerGroup;       
  3. //消息消费模式
  4. private MessageModel messageModel = MessageModel.CLUSTERING;       
  5. //指定消费开始偏移量(最大偏移量、最小偏移量、启动时间戳)开始消费
  6. private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
  7. //集群模式下的消息队列负载策略
  8. private AllocateMessageQueueStrategy allocateMessageQueueStrategy;
  9. //订阅信息
  10. private Map<String /* topic */, String /* sub expression */> subscription = new HashMap<String, String>();
  11. //消息业务监听器
  12. private MessageListener messageListener;
  13. //消息消费进度存储器
  14. private OffsetStore offsetStore;
  15. //消费者最小线程数量
  16. private int consumeThreadMin = 20;
  17. //消费者最大线程数量
  18. private int consumeThreadMax = 20;
  19. //并发消息消费时处理队列最大跨度
  20. private int consumeConcurrentlyMaxSpan = 2000;
  21. //每1000次流控后打印流控日志
  22. private int pullThresholdForQueue = 1000;
  23. //推模式下任务间隔时间
  24. private long pullInterval = 0;
  25. //推模式下任务拉取的条数,默认32条
  26. private int pullBatchSize = 32;
  27. //每次传入MessageListener#consumerMessage中消息的数量
  28. private int consumeMessageBatchMaxSize = 1;
  29. //是否每次拉取消息都订阅消息
  30. private boolean postSubscriptionWhenPull = false;
  31. //消息重试次数,-1代表16次
  32. private int maxReconsumeTimes = -1;
  33. //消息消费超时时间
  34. private long consumeTimeout = 15;
复制代码
消费者启动流程

19.png

代码:DefaultMQPushConsumerImpl#start
  1. public synchronized void start() throws MQClientException {
  2.     switch (this.serviceState) {
  3.         case CREATE_JUST:
  4.             
  5.                 this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
  6.             this.serviceState = ServiceState.START_FAILED;
  7.                         //检查消息者是否合法
  8.             this.checkConfig();
  9.                         //构建主题订阅信息
  10.             this.copySubscription();
  11.                         //设置消费者客户端实例名称为进程ID
  12.             if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
  13.                 this.defaultMQPushConsumer.changeInstanceNameToPID();
  14.             }
  15.                         //创建MQClient实例
  16.             this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
  17.                         //构建rebalanceImpl
  18.             this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
  19.             this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
  20.             this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
  21.             this.rebalanceImpl.setmQClientFactory(this.mQClientFactor
  22.             this.pullAPIWrapper = new PullAPIWrapper(
  23.                 mQClientFactory,
  24.                 this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
  25.             this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookLis
  26.             if (this.defaultMQPushConsumer.getOffsetStore() != null) {
  27.                 this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
  28.             } else {
  29.                            switch (this.defaultMQPushConsumer.getMessageModel()) {
  30.                
  31.                        case BROADCASTING:         //消息消费广播模式,将消费进度保存在本地
  32.                            this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
  33.                                break;
  34.                            case CLUSTERING:        //消息消费集群模式,将消费进度保存在远端Broker
  35.                                this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
  36.                                break;
  37.                            default:
  38.                                break;
  39.                        }
  40.                        this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
  41.                    }
  42.             this.offsetStore.load
  43.             //创建顺序消息消费服务
  44.             if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
  45.                 this.consumeOrderly = true;
  46.                 this.consumeMessageService =
  47.                     new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
  48.                 //创建并发消息消费服务
  49.             } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
  50.                 this.consumeOrderly = false;
  51.                 this.consumeMessageService =
  52.                     new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
  53.             }
  54.             //消息消费服务启动
  55.             this.consumeMessageService.start();
  56.             //注册消费者实例
  57.             boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
  58.             
  59.             if (!registerOK) {
  60.                 this.serviceState = ServiceState.CREATE_JUST;
  61.                 this.consumeMessageService.shutdown();
  62.                 throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
  63.                     + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
  64.                     null);
  65.             //启动消费者客户端
  66.             mQClientFactory.start();
  67.             log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
  68.             this.serviceState = ServiceState.RUNNING;
  69.             break;
  70.             case RUNNING:
  71.             case START_FAILED:
  72.         case SHUTDOWN_ALREADY:
  73.             throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
  74.                 + this.serviceState
  75.                 + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
  76.                 null);
  77.         default:
  78.             break;
  79.     }
  80.     this.updateTopicSubscribeInfoWhenSubscriptionChanged();
  81.     this.mQClientFactory.checkClientInBroker();
  82.     this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
  83.     this.mQClientFactory.rebalanceImmediately();
  84. }
复制代码
消息拉取

消息消费模式有两种模式:广播模式与集群模式。广播模式比较简单,每一个消费者需要拉取订阅主题下所有队列的消息。本文重点讲解集群模式。在集群模式下,同一个消费者组内有多个消息消费者,同一个主题存在多个消费队列,消费者通过负载均衡的方式消费消息。
消息队列负载均衡,通常的作法是一个消息队列在同一个时间只允许被一个消费消费者消费,一个消息消费者可以同时消费多个消息队列。
1)PullMessageService实现机制

从MQClientInstance的启动流程中可以看出,RocketMQ使用一个单独的线程PullMessageService来负责消息的拉取。
20.png

代码:PullMessageService#run
  1. public void run() {
  2.     log.info(this.getServiceName() + " service started");
  3.         //循环拉取消息
  4.     while (!this.isStopped()) {
  5.         try {
  6.             //从请求队列中获取拉取消息请求
  7.             PullRequest pullRequest = this.pullRequestQueue.take();
  8.             //拉取消息
  9.             this.pullMessage(pullRequest);
  10.         } catch (InterruptedException ignored) {
  11.         } catch (Exception e) {
  12.             log.error("Pull Message Service Run Method exception", e);
  13.         }
  14.     }
  15.     log.info(this.getServiceName() + " service end");
  16. }
复制代码
PullRequest
21.png
  1. private String consumerGroup;        //消费者组
  2. private MessageQueue messageQueue;        //待拉取消息队列
  3. private ProcessQueue processQueue;        //消息处理队列
  4. private long nextOffset;        //待拉取的MessageQueue偏移量
  5. private boolean lockedFirst = false;        //是否被锁定
复制代码
代码:PullMessageService#pullMessage
  1. private void pullMessage(final PullRequest pullRequest) {
  2.     //获得消费者实例
  3.     final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
  4.     if (consumer != null) {
  5.         //强转为推送模式消费者
  6.         DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
  7.         //推送消息
  8.         impl.pullMessage(pullRequest);
  9.     } else {
  10.         log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
  11.     }
  12. }
复制代码
2)ProcessQueue实现机制

ProcessQueue是MessageQueue在消费端的重现、快照。PullMessageService从消息服务器默认每次拉取32条消息,按照消息的队列偏移量顺序存放在ProcessQueue中,PullMessageService然后将消息提交到消费者消费线程池,消息成功消费后从ProcessQueue中移除。
22.png

属性
  1. //消息容器
  2. private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
  3. //读写锁
  4. private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock();
  5. //ProcessQueue总消息树
  6. private final AtomicLong msgCount = new AtomicLong();
  7. //ProcessQueue队列最大偏移量
  8. private volatile long queueOffsetMax = 0L;
  9. //当前ProcessQueue是否被丢弃
  10. private volatile boolean dropped = false;
  11. //上一次拉取时间戳
  12. private volatile long lastPullTimestamp = System.currentTimeMillis();
  13. //上一次消费时间戳
  14. private volatile long lastConsumeTimestamp = System.currentTimeMillis();
复制代码
方法
  1. //移除消费超时消息
  2. public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer)
  3. //添加消息
  4. public boolean putMessage(final List<MessageExt> msgs)
  5. //获取消息最大间隔
  6. public long getMaxSpan()
  7. //移除消息
  8. public long removeMessage(final List<MessageExt> msgs)
  9. //将consumingMsgOrderlyTreeMap中消息重新放在msgTreeMap,并清空consumingMsgOrderlyTreeMap   
  10. public void rollback()
  11. //将consumingMsgOrderlyTreeMap消息清除,表示成功处理该批消息
  12. public long commit()
  13. //重新处理该批消息
  14. public void makeMessageToCosumeAgain(List<MessageExt> msgs)
  15. //从processQueue中取出batchSize条消息
  16. public List<MessageExt> takeMessags(final int batchSize)
复制代码
3)消息拉取基本流程

客户端发起拉取请求

23.png

代码:DefaultMQPushConsumerImpl#pullMessage
  1. public void pullMessage(final PullRequest pullRequest) {
  2.     //从pullRequest获得ProcessQueue
  3.     final ProcessQueue processQueue = pullRequest.getProcessQueue();
  4.     //如果处理队列被丢弃,直接返回
  5.     if (processQueue.isDropped()) {
  6.         log.info("the pull request[{}] is dropped.", pullRequest.toString());
  7.         return;
  8.     }
  9.         //如果处理队列未被丢弃,更新时间戳
  10.     pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
  11.     try {
  12.         this.makeSureStateOK();
  13.     } catch (MQClientException e) {
  14.         log.warn("pullMessage exception, consumer state not ok", e);
  15.         this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
  16.         return;
  17.     }
  18.         //如果处理队列被挂起,延迟1s后再执行
  19.     if (this.isPause()) {
  20.         log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
  21.         this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
  22.         return;
  23.     }
  24.         //获得最大待处理消息数量
  25.         long cachedMessageCount = processQueue.getMsgCount().get();
  26.     //获得最大待处理消息大小
  27.         long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
  28.         //从数量进行流控
  29.         if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
  30.             this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
  31.             if ((queueFlowControlTimes++ % 1000) == 0) {
  32.                 log.warn(
  33.                     "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
  34.                     this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
  35.             }
  36.             return;
  37.         }
  38.         //从消息大小进行流控
  39.         if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
  40.             this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
  41.             if ((queueFlowControlTimes++ % 1000) == 0) {
  42.                 log.warn(
  43.                     "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
  44.                     this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
  45.             }
  46.             return;
  47.     }
  48.             //获得订阅信息
  49.                  final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
  50.             if (null == subscriptionData) {
  51.                 this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
  52.                 log.warn("find the consumer's subscription failed, {}", pullRequest);
  53.                 return;
  54.                 //与服务端交互,获取消息
  55.             this.pullAPIWrapper.pullKernelImpl(
  56.             pullRequest.getMessageQueue(),
  57.             subExpression,
  58.             subscriptionData.getExpressionType(),
  59.             subscriptionData.getSubVersion(),
  60.             pullRequest.getNextOffset(),
  61.             this.defaultMQPushConsumer.getPullBatchSize(),
  62.             sysFlag,
  63.             commitOffsetValue,
  64.             BROKER_SUSPEND_MAX_TIME_MILLIS,
  65.             CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
  66.             CommunicationMode.ASYNC,
  67.             pullCallback
  68.         );
  69.             
  70. }
复制代码
消息服务端Broker组装消息

24.png

代码:PullMessageProcessor#processRequest
  1. //构建消息过滤器
  2. MessageFilter messageFilter;
  3. if (this.brokerController.getBrokerConfig().isFilterSupportRetry()) {
  4.     messageFilter = new ExpressionForRetryMessageFilter(subscriptionData, consumerFilterData,
  5.         this.brokerController.getConsumerFilterManager());
  6. } else {
  7.     messageFilter = new ExpressionMessageFilter(subscriptionData, consumerFilterData,
  8.         this.brokerController.getConsumerFilterManager());
  9. }
  10. //调用MessageStore.getMessage查找消息
  11. final GetMessageResult getMessageResult =
  12.     this.brokerController.getMessageStore().getMessage(
  13.                                     requestHeader.getConsumerGroup(), //消费组名称                                                               
  14.                                     requestHeader.getTopic(),        //主题名称
  15.                                 requestHeader.getQueueId(), //队列ID
  16.                                     requestHeader.getQueueOffset(),         //待拉取偏移量
  17.                                     requestHeader.getMaxMsgNums(),         //最大拉取消息条数
  18.                                     messageFilter        //消息过滤器
  19.                     );
复制代码
代码:DefaultMessageStore#getMessage
  1. GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
  2. long nextBeginOffset = offset;        //查找下一次队列偏移量
  3. long minOffset = 0;                //当前消息队列最小偏移量
  4. long maxOffset = 0;                //当前消息队列最大偏移量
  5. GetMessageResult getResult = new GetMessageResult();
  6. final long maxOffsetPy = this.commitLog.getMaxOffset();        //当前commitLog最大偏移量
  7. //根据主题名称和队列编号获取消息消费队列
  8. ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
  9. ...
  10. minOffset = consumeQueue.getMinOffsetInQueue();
  11. maxOffset = consumeQueue.getMaxOffsetInQueue();
  12. //消息偏移量异常情况校对下一次拉取偏移量
  13. if (maxOffset == 0) {        //表示当前消息队列中没有消息
  14.     status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
  15.     nextBeginOffset = nextOffsetCorrection(offset, 0);
  16. } else if (offset < minOffset) {        //待拉取消息的偏移量小于队列的其实偏移量
  17.     status = GetMessageStatus.OFFSET_TOO_SMALL;
  18.     nextBeginOffset = nextOffsetCorrection(offset, minOffset);
  19. } else if (offset == maxOffset) {        //待拉取偏移量为队列最大偏移量
  20.     status = GetMessageStatus.OFFSET_OVERFLOW_ONE;
  21.     nextBeginOffset = nextOffsetCorrection(offset, offset);
  22. } else if (offset > maxOffset) {        //偏移量越界
  23.     status = GetMessageStatus.OFFSET_OVERFLOW_BADLY;
  24.     if (0 == minOffset) {
  25.         nextBeginOffset = nextOffsetCorrection(offset, minOffset);
  26.     } else {
  27.         nextBeginOffset = nextOffsetCorrection(offset, maxOffset);
  28.     }
  29. }
  30. ...
  31. //根据偏移量从CommitLog中拉取32条消息
  32. SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);
复制代码
代码:PullMessageProcessor#processRequest
  1. //根据拉取结果填充responseHeader
  2. response.setRemark(getMessageResult.getStatus().name());
  3. responseHeader.setNextBeginOffset(getMessageResult.getNextBeginOffset());
  4. responseHeader.setMinOffset(getMessageResult.getMinOffset());
  5. responseHeader.setMaxOffset(getMessageResult.getMaxOffset());
  6. //判断如果存在主从同步慢,设置下一次拉取任务的ID为主节点
  7. switch (this.brokerController.getMessageStoreConfig().getBrokerRole()) {
  8.     case ASYNC_MASTER:
  9.     case SYNC_MASTER:
  10.         break;
  11.     case SLAVE:
  12.         if (!this.brokerController.getBrokerConfig().isSlaveReadEnable()) {
  13.             response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);
  14.             responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID);
  15.         }
  16.         break;
  17. }
  18. ...
  19. //GetMessageResult与Response的Code转换
  20. switch (getMessageResult.getStatus()) {
  21.     case FOUND:                        //成功
  22.         response.setCode(ResponseCode.SUCCESS);
  23.         break;
  24.     case MESSAGE_WAS_REMOVING:        //消息存放在下一个commitLog中
  25.         response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);        //消息重试
  26.         break;
  27.     case NO_MATCHED_LOGIC_QUEUE:        //未找到队列
  28.     case NO_MESSAGE_IN_QUEUE:        //队列中未包含消息
  29.         if (0 != requestHeader.getQueueOffset()) {
  30.             response.setCode(ResponseCode.PULL_OFFSET_MOVED);
  31.             requestHeader.getQueueOffset(),
  32.             getMessageResult.getNextBeginOffset(),
  33.             requestHeader.getTopic(),
  34.             requestHeader.getQueueId(),
  35.             requestHeader.getConsumerGroup()
  36.             );
  37.         } else {
  38.             response.setCode(ResponseCode.PULL_NOT_FOUND);
  39.         }
  40.         break;
  41.     case NO_MATCHED_MESSAGE:        //未找到消息
  42.         response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);
  43.         break;
  44.     case OFFSET_FOUND_NULL:        //消息物理偏移量为空
  45.         response.setCode(ResponseCode.PULL_NOT_FOUND);
  46.         break;
  47.     case OFFSET_OVERFLOW_BADLY:        //offset越界
  48.         response.setCode(ResponseCode.PULL_OFFSET_MOVED);
  49.         // XXX: warn and notify me
  50.         log.info("the request offset: {} over flow badly, broker max offset: {}, consumer: {}",
  51.                 requestHeader.getQueueOffset(), getMessageResult.getMaxOffset(), channel.remoteAddress());
  52.         break;
  53.     case OFFSET_OVERFLOW_ONE:        //offset在队列中未找到
  54.         response.setCode(ResponseCode.PULL_NOT_FOUND);
  55.         break;
  56.     case OFFSET_TOO_SMALL:        //offset未在队列中
  57.         response.setCode(ResponseCode.PULL_OFFSET_MOVED);
  58.         requestHeader.getConsumerGroup(),
  59.         requestHeader.getTopic(),
  60.         requestHeader.getQueueOffset(),
  61.         getMessageResult.getMinOffset(), channel.remoteAddress());
  62.         break;
  63.     default:
  64.         assert false;
  65.         break;
  66. }
  67. ...
  68. //如果CommitLog标记可用,并且当前Broker为主节点,则更新消息消费进度
  69. boolean storeOffsetEnable = brokerAllowSuspend;
  70. storeOffsetEnable = storeOffsetEnable && hasCommitOffsetFlag;
  71. storeOffsetEnable = storeOffsetEnable
  72.     && this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE;
  73. if (storeOffsetEnable) {
  74.     this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel),
  75.         requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset());
  76. }
复制代码
消息拉取客户端处理消息

25.png

代码:MQClientAPIImpl#processPullResponse
  1. private PullResult processPullResponse(
  2.     final RemotingCommand response) throws MQBrokerException, RemotingCommandException {
  3.     PullStatus pullStatus = PullStatus.NO_NEW_MSG;
  4.            //判断响应结果
  5.     switch (response.getCode()) {
  6.         case ResponseCode.SUCCESS:
  7.             pullStatus = PullStatus.FOUND;
  8.             break;
  9.         case ResponseCode.PULL_NOT_FOUND:
  10.             pullStatus = PullStatus.NO_NEW_MSG;
  11.             break;
  12.         case ResponseCode.PULL_RETRY_IMMEDIATELY:
  13.             pullStatus = PullStatus.NO_MATCHED_MSG;
  14.             break;
  15.         case ResponseCode.PULL_OFFSET_MOVED:
  16.             pullStatus = PullStatus.OFFSET_ILLEGAL;
  17.             break;
  18.         default:
  19.             throw new MQBrokerException(response.getCode(), response.getRemark());
  20.     }
  21.         //解码响应头
  22.     PullMessageResponseHeader responseHeader =
  23.         (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class);
  24.         //封装PullResultExt返回
  25.     return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(),
  26.         responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody());
  27. }
复制代码
PullResult类
  1. private final PullStatus pullStatus;        //拉取结果
  2. private final long nextBeginOffset;        //下次拉取偏移量
  3. private final long minOffset;        //消息队列最小偏移量
  4. private final long maxOffset;        //消息队列最大偏移量
  5. private List<MessageExt> msgFoundList;        //拉取的消息列表
复制代码
26.png

代码:DefaultMQPushConsumerImpl$PullCallback#OnSuccess
  1. //将拉取到的消息存入processQueue
  2. boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
  3. //将processQueue提交到consumeMessageService中供消费者消费
  4. DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
  5.     pullResult.getMsgFoundList(),
  6.     processQueue,
  7.     pullRequest.getMessageQueue(),
  8.     dispatchToConsume);
  9. //如果pullInterval大于0,则等待pullInterval毫秒后将pullRequest对象放入到PullMessageService中的pullRequestQueue队列中
  10. if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
  11.     DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
  12.         DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
  13. } else {
  14.     DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
  15. }
复制代码
消息拉取总结

27.png

4)消息拉取长轮询机制分析

RocketMQ未真正实现消息推模式,而是消费者主动向消息服务器拉取消息,RocketMQ推模式是循环向消息服务端发起消息拉取请求,如果消息消费者向RocketMQ拉取消息时,消息未到达消费队列时,如果不启用长轮询机制,则会在服务端等待shortPollingTimeMills时间后(挂起)再去判断消息是否已经到达指定消息队列,如果消息仍未到达则提示拉取消息客户端PULL—NOT—FOUND(消息不存在);如果开启长轮询模式,RocketMQ一方面会每隔5s轮询检查一次消息是否可达,同时一有消息达到后立马通知挂起线程再次验证消息是否是自己感兴趣的消息,如果是则从CommitLog文件中提取消息返回给消息拉取客户端,否则直到挂起超时,超时时间由消息拉取方在消息拉取是封装在请求参数中,PUSH模式为15s,PULL模式通过DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis设置。RocketMQ通过在Broker客户端配置longPollingEnable为true来开启长轮询模式。
代码:PullMessageProcessor#processRequest
  1. //当没有拉取到消息时,通过长轮询方式继续拉取消息
  2. case ResponseCode.PULL_NOT_FOUND:
  3.     if (brokerAllowSuspend && hasSuspendFlag) {
  4.         long pollingTimeMills = suspendTimeoutMillisLong;
  5.         if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
  6.             pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
  7.         }
  8.         String topic = requestHeader.getTopic();
  9.         long offset = requestHeader.getQueueOffset();
  10.         int queueId = requestHeader.getQueueId();
  11.         //构建拉取请求对象
  12.         PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
  13.             this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
  14.         //处理拉取请求
  15.         this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);
  16.         response = null;
  17.         break;
  18.     }
复制代码
PullRequestHoldService方式实现长轮询
代码:PullRequestHoldService#suspendPullRequest
  1. //将拉取消息请求,放置在ManyPullRequest集合中
  2. public void suspendPullRequest(final String topic, final int queueId, final PullRequest pullRequest) {
  3.     String key = this.buildKey(topic, queueId);
  4.     ManyPullRequest mpr = this.pullRequestTable.get(key);
  5.     if (null == mpr) {
  6.         mpr = new ManyPullRequest();
  7.         ManyPullRequest prev = this.pullRequestTable.putIfAbsent(key, mpr);
  8.         if (prev != null) {
  9.             mpr = prev;
  10.         }
  11.     }
  12.     mpr.addPullRequest(pullRequest);
  13. }
复制代码
代码:PullRequestHoldService#run
  1. public void run() {
  2.     log.info("{} service started", this.getServiceName());
  3.     while (!this.isStopped()) {
  4.         try {
  5.             //如果开启长轮询每隔5秒判断消息是否到达
  6.             if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
  7.                 this.waitForRunning(5 * 1000);
  8.             } else {
  9.                 //没有开启长轮询,每隔1s再次尝试
  10.               this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
  11.             }
  12.             long beginLockTimestamp = this.systemClock.now();
  13.             this.checkHoldRequest();
  14.             long costTime = this.systemClock.now() - beginLockTimestamp;
  15.             if (costTime > 5 * 1000) {
  16.                 log.info("[NOTIFYME] check hold request cost {} ms.", costTime);
  17.             }
  18.         } catch (Throwable e) {
  19.             log.warn(this.getServiceName() + " service has exception. ", e);
  20.         }
  21.     }
  22.     log.info("{} service end", this.getServiceName());
  23. }
复制代码
代码:PullRequestHoldService#checkHoldRequest
  1. //遍历拉取任务
  2. private void checkHoldRequest() {
  3.     for (String key : this.pullRequestTable.keySet()) {
  4.         String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR);
  5.         if (2 == kArray.length) {
  6.             String topic = kArray[0];
  7.             int queueId = Integer.parseInt(kArray[1]);
  8.             //获得消息偏移量
  9.             final long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
  10.             try {
  11.                 //通知有消息达到
  12.                 this.notifyMessageArriving(topic, queueId, offset);
  13.             } catch (Throwable e) {
  14.                 log.error("check hold request failed. topic={}, queueId={}", topic, queueId, e);
  15.             }
  16.         }
  17.     }
  18. }
复制代码
代码:PullRequestHoldService#notifyMessageArriving
  1. //如果拉取消息偏移大于请求偏移量,如果消息匹配调用executeRequestWhenWakeup处理消息
  2. if (newestOffset > request.getPullFromThisOffset()) {
  3.     boolean match = request.getMessageFilter().isMatchedByConsumeQueue(tagsCode,
  4.         new ConsumeQueueExt.CqExtUnit(tagsCode, msgStoreTime, filterBitMap));
  5.     // match by bit map, need eval again when properties is not null.
  6.     if (match && properties != null) {
  7.         match = request.getMessageFilter().isMatchedByCommitLog(null, properties);
  8.     }
  9.     if (match) {
  10.         try {
  11.             this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
  12.                 request.getRequestCommand());
  13.         } catch (Throwable e) {
  14.             log.error("execute request when wakeup failed.", e);
  15.         }
  16.         continue;
  17.     }
  18. }
  19. //如果过期时间超时,则不继续等待将直接返回给客户端消息未找到
  20. if (System.currentTimeMillis() >= (request.getSuspendTimestamp() + request.getTimeoutMillis())) {
  21.     try {
  22.         this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
  23.             request.getRequestCommand());
  24.     } catch (Throwable e) {
  25.         log.error("execute request when wakeup failed.", e);
  26.     }
  27.     continue;
  28. }
复制代码
如果开启了长轮询机制,PullRequestHoldService会每隔5s被唤醒去尝试检测是否有新的消息的到来才给客户端响应,或者直到超时才给客户端进行响应,消息实时性比较差,为了避免这种情况,RocketMQ引入另外一种机制:当消息到达时唤醒挂起线程触发一次检查。
DefaultMessageStore$ReputMessageService机制
代码:DefaultMessageStore#start
  1. //长轮询入口
  2. this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
  3. this.reputMessageService.start();
复制代码
代码:DefaultMessageStore$ReputMessageService#run
  1. public void run() {
  2.     DefaultMessageStore.log.info(this.getServiceName() + " service started");
  3.     while (!this.isStopped()) {
  4.         try {
  5.             Thread.sleep(1);
  6.             //长轮询核心逻辑代码入口
  7.             this.doReput();
  8.         } catch (Exception e) {
  9.             DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
  10.         }
  11.     }
  12.     DefaultMessageStore.log.info(this.getServiceName() + " service end");
  13. }
复制代码
代码:DefaultMessageStore$ReputMessageService#deReput
  1. //当新消息达到是,进行通知监听器进行处理
  2. if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
  3.     && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
  4.     DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
  5.         dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
  6.         dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
  7.         dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
  8. }
复制代码
代码:NotifyMessageArrivingListener#arriving
  1. public void arriving(String topic, int queueId, long logicOffset, long tagsCode,
  2.     long msgStoreTime, byte[] filterBitMap, Map<String, String> properties) {
  3.     this.pullRequestHoldService.notifyMessageArriving(topic, queueId, logicOffset, tagsCode,
  4.         msgStoreTime, filterBitMap, properties);
  5. }
复制代码
消息队列负载与重新分布机制

RocketMQ消息队列重新分配是由RebalanceService线程来实现。一个MQClientInstance持有一个RebalanceService实现,并随着MQClientInstance的启动而启动。
代码:RebalanceService#run
  1. public void run() {
  2.     log.info(this.getServiceName() + " service started");
  3.         //RebalanceService线程默认每隔20s执行一次mqClientFactory.doRebalance方法
  4.     while (!this.isStopped()) {
  5.         this.waitForRunning(waitInterval);
  6.         this.mqClientFactory.doRebalance();
  7.     }
  8.     log.info(this.getServiceName() + " service end");
  9. }
复制代码
代码:MQClientInstance#doRebalance
  1. public void doRebalance() {
  2.     //MQClientInstance遍历以注册的消费者,对消费者执行doRebalance()方法
  3.     for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
  4.         MQConsumerInner impl = entry.getValue();
  5.         if (impl != null) {
  6.             try {
  7.                 impl.doRebalance();
  8.             } catch (Throwable e) {
  9.                 log.error("doRebalance exception", e);
  10.             }
  11.         }
  12.     }
  13. }
复制代码
代码:RebalanceImpl#doRebalance
  1. //遍历订阅消息对每个主题的订阅的队列进行重新负载
  2. public void doRebalance(final boolean isOrder) {
  3.     Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
  4.     if (subTable != null) {
  5.         for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
  6.             final String topic = entry.getKey();
  7.             try {
  8.                 this.rebalanceByTopic(topic, isOrder);
  9.             } catch (Throwable e) {
  10.                 if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
  11.                     log.warn("rebalanceByTopic Exception", e);
  12.                 }
  13.             }
  14.         }
  15.     }
  16.     this.truncateMessageQueueNotMyTopic();
  17. }
复制代码
代码:RebalanceImpl#rebalanceByTopic
  1. //从主题订阅消息缓存表中获取主题的队列信息
  2. Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
  3. //查找该主题订阅组所有的消费者ID
  4. List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
  5. //给消费者重新分配队列
  6. if (mqSet != null && cidAll != null) {
  7.     List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
  8.     mqAll.addAll(mqSet);
  9.     Collections.sort(mqAll);
  10.     Collections.sort(cidAll);
  11.     AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
  12.     List<MessageQueue> allocateResult = null;
  13.     try {
  14.         allocateResult = strategy.allocate(
  15.             this.consumerGroup,
  16.             this.mQClientFactory.getClientId(),
  17.             mqAll,
  18.             cidAll);
  19.     } catch (Throwable e) {
  20.         log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
  21.             e);
  22.         return;
  23.     }
复制代码
RocketMQ默认提供5中负载均衡分配算法
  1. AllocateMessageQueueAveragely:平均分配
  2. 举例:8个队列q1,q2,q3,q4,q5,a6,q7,q8,消费者3个:c1,c2,c3
  3. 分配如下:
  4. c1:q1,q2,q3
  5. c2:q4,q5,a6
  6. c3:q7,q8
  7. AllocateMessageQueueAveragelyByCircle:平均轮询分配
  8. 举例:8个队列q1,q2,q3,q4,q5,a6,q7,q8,消费者3个:c1,c2,c3
  9. 分配如下:
  10. c1:q1,q4,q7
  11. c2:q2,q5,a8
  12. c3:q3,q6
复制代码
注意:消息队列的分配遵循一个消费者可以分配到多个队列,但同一个消息队列只会分配给一个消费者,故如果出现消费者个数大于消息队列数量,则有些消费者无法消费消息。
消息消费过程

PullMessageService负责对消息队列进行消息拉取,从远端服务器拉取消息后将消息存储ProcessQueue消息队列处理队列中,然后调用ConsumeMessageService#submitConsumeRequest方法进行消息消费,使用线程池来消费消息,确保了消息拉取与消息消费的解耦。ConsumeMessageService支持顺序消息和并发消息,核心类图如下:
28.png

并发消息消费
代码:ConsumeMessageConcurrentlyService#submitConsumeRequest
[code]//消息批次单次final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();//msgs.size()默认最多为32条。//如果msgs.size()小于consumeBatchSize,则直接将拉取到的消息放入到consumeRequest,然后将consumeRequest提交到消费者线程池中if (msgs.size()
您需要登录后才可以回帖 登录 | 立即注册