/** * Copyright (C) 2010-2013 Alibaba Group Holding Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.alibaba.rocketmq.store; import java.io.File; import java.nio.ByteBuffer; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.alibaba.rocketmq.common.constant.LoggerName; /** * 消费队列实现 * * @author shijia.wxr<vintage.wang@gmail.com> * @since 2013-7-21 */ public class ConsumeQueue { // 存储单元大小 public static final int CQStoreUnitSize = 20; private static final Logger log = LoggerFactory.getLogger(LoggerName.StoreLoggerName); // 存储顶层对象 private final DefaultMessageStore defaultMessageStore; // 存储消息索引的队列 private final MapedFileQueue mapedFileQueue; // Topic private final String topic; // queueId private final int queueId; // 写索引时用到的ByteBuffer private final ByteBuffer byteBufferIndex; // 配置 private final String storePath; private final int mapedFileSize; // 最后一个消息对应的物理Offset /** * chen.si: 最后一个逻辑消息对应的物理消息的起始Offset */ private long maxPhysicOffset = -1; // 逻辑队列的最小Offset,删除物理文件时,计算出来的最小Offset // 实际使用需要除以 StoreUnitSize /** * chen.si 此队列的最小字节offset,实际上就是 第1个mapped file的第1个有效消息的global offset(第1个文件可能被特殊20字节填充的,所以是第1条有效消息) * 一般情况下就是第1个mapped file的fileoffset * * 实际上是标识 索引队列 的1条有效消息,那说明存在无效消息,无效消息有2种: * 1. 填充的 特殊20字节 的消息(索引消息逻辑错乱导致) * 2. 对应的物理消息(即:commit log)已经被删除 */ private volatile long minLogicOffset = 0; public ConsumeQueue(// final String topic,// final int queueId,// final String storePath,// final int mapedFileSize,// final DefaultMessageStore defaultMessageStore) { this.storePath = storePath; this.mapedFileSize = mapedFileSize; this.defaultMessageStore = defaultMessageStore; this.topic = topic; this.queueId = queueId; String queueDir = this.storePath// + File.separator + topic// + File.separator + queueId;// this.mapedFileQueue = new MapedFileQueue(queueDir, mapedFileSize, null); this.byteBufferIndex = ByteBuffer.allocate(CQStoreUnitSize); } public boolean load() { /** * chen.si 加载 <topic>/<queueid>下的 maped files */ boolean result = this.mapedFileQueue.load(); log.info("load consume queue " + this.topic + "-" + this.queueId + " " + (result ? "OK" : "Failed")); return result; } public void recover() { final List<MapedFile> mapedFiles = this.mapedFileQueue.getMapedFiles(); if (!mapedFiles.isEmpty()) { /** * chen.si 老样子,从倒数第3个开始恢复 */ // 从倒数第三个文件开始恢复 int index = mapedFiles.size() - 3; if (index < 0) index = 0; int mapedFileSizeLogics = this.mapedFileSize; MapedFile mapedFile = mapedFiles.get(index); ByteBuffer byteBuffer = mapedFile.sliceByteBuffer(); //chen.si global offset long processOffset = mapedFile.getFileFromOffset(); //chen.si local offset long mapedFileOffset = 0; while (true) { for (int i = 0; i < mapedFileSizeLogics; i += CQStoreUnitSize) { // chen.si: 8 bytes for offset long offset = byteBuffer.getLong(); // chen.si: 4 bytes for size int size = byteBuffer.getInt(); // chen.si: 8 bytes for tags code long tagsCode = byteBuffer.getLong(); // 说明当前存储单元有效 // TODO 这样判断有效是否合理? if (offset >= 0 && size > 0) { /** * chen.si 看是否到了最后一条消息 */ mapedFileOffset = i + CQStoreUnitSize; /** * chen.si cq的消息在commit log的最大offset */ this.maxPhysicOffset = offset; } else { /** * chen.si 文件到了最后一条消息,结束这个文件(有2种结束条件:1. 文件未写满,而结束 2. 文件写满,同时也没有写下一个文件) * * 这里是第1个条件 : 1. 文件未写满,而结束 */ log.info("recover current consume queue file over, " + mapedFile.getFileName() + " " + offset + " " + size + " " + tagsCode); break; } } /** * chen.si 只能通过local offset 和 文件满期望的 大小 来 判断 文件是否满(commit log有单独的结束标识) * 说明这个文件是因为文件满才结束的,需要继续恢复下一个文件(如果有的话) * */ // 走到文件末尾,切换至下一个文件 if (mapedFileOffset == mapedFileSizeLogics) { index++; /** * 这里是第2个条件 :2. 文件写满,同时也没有写下一个文件 */ if (index >= mapedFiles.size()) { // 当前条件分支不可能发生 log.info("recover last consume queue file over, last maped file " + mapedFile.getFileName()); break; } else { /** * chen.si 准备下一个文件 */ mapedFile = mapedFiles.get(index); byteBuffer = mapedFile.sliceByteBuffer(); processOffset = mapedFile.getFileFromOffset(); mapedFileOffset = 0; log.info("recover next consume queue file, " + mapedFile.getFileName()); } } else { log.info("recover current consume queue queue over " + mapedFile.getFileName() + " " + (processOffset + mapedFileOffset)); break; } } /** * 计算最大的global offset */ processOffset += mapedFileOffset; /** * chen.si 为什么没有设置committedWhere? * * processOffset实际上为最后一个有效消息的结束地址。这个地址之后的文件,都是无效的 */ this.mapedFileQueue.truncateDirtyFiles(processOffset); } } /** * 二分查找查找消息发送时间最接近timestamp逻辑队列的offset */ public long getOffsetInQueueByTime(final long timestamp) { MapedFile mapedFile = this.mapedFileQueue.getMapedFileByTime(timestamp); if (mapedFile != null) { long offset = 0; // low:第一个索引信息的起始位置 // minLogicOffset有设置值则从 // minLogicOffset-mapedFile.getFileFromOffset()位置开始才是有效值 /** * chen.si: 索引文件中,之前提到过,为了避免逻辑顺序错误,增加了在文件开始填充 特殊20字节,来调整 保证 索引消息位置 符合 实际位置 * * 所以文件开始X字节,可能不是真实的索引消息, 需要跳到第1条真实的消息:minLogicOffset-mapedFile.getFileFromOffset() * * 其中,minLogicOffset是 位置的实际偏移字节量 */ /** * chen.si low为相对于文件的起始的local offset(字节偏移) * * 目的: 找到当前maped file的第1条有效消息的位置 * * 如果minLogicOffset > mapedFile.getFileFromOffset(), 说明 这个mapedFile的刚开始部分存在 特殊20字节,所以需要去掉,不能从0计算有效消息。 * 其实也就是说,当前这个文件是queue的第1个文件。 否则的话,说明 这个mapedfile 不是第1个文件,就不需要考虑minLogicOffset */ int low = minLogicOffset > mapedFile.getFileFromOffset() ? (int) (minLogicOffset - mapedFile .getFileFromOffset()) : 0; // high:最后一个索引信息的起始位置 int high = 0; int midOffset = -1, targetOffset = -1, leftOffset = -1, rightOffset = -1; long leftIndexValue = -1L, rightIndexValue = -1L; // 取出该mapedFile里面所有的映射空间(没有映射的空间并不会返回,不会返回文件空洞) SelectMapedBufferResult sbr = mapedFile.selectMapedBuffer(0); if (null != sbr) { ByteBuffer byteBuffer = sbr.getByteBuffer(); /** * chen.si 最后一个索引消息的 起始 字节位置, 所以 减去 20 */ high = byteBuffer.limit() - CQStoreUnitSize; try { while (high >= low) { /** * chen.si binary search 二分查找的基础: 索引文件中的消息,是按照store timestamp时间排序的 */ midOffset = (low + high) / (2 * CQStoreUnitSize) * CQStoreUnitSize; byteBuffer.position(midOffset); long phyOffset = byteBuffer.getLong(); int size = byteBuffer.getInt(); // 比较时间, 折半 /** * chen.si 根据索引消息的commit phy offset和size字段,在commit log中找到这条消息的store timestamp信息 */ long storeTime = this.defaultMessageStore.getCommitLog().pickupStoretimestamp(phyOffset, size); if (storeTime < 0) { // 没有从物理文件找到消息,此时直接返回0 return 0; } else if (storeTime == timestamp) { /** * chen.si 正好找到,查找结束,直接break掉 */ targetOffset = midOffset; break; } else if (storeTime > timestamp) { /** * chen.si 找到了 大 的值,记下来。 再在 左边 binary search */ high = midOffset - CQStoreUnitSize; rightOffset = midOffset; rightIndexValue = storeTime; } else { /** * chen.si 找到了 小 的值,记下来。 再在 右边 binary search */ low = midOffset + CQStoreUnitSize; leftOffset = midOffset; leftIndexValue = storeTime; } } if (targetOffset != -1) { /** * chen.si 找到了 相等的,之前break出来的 */ // 查询的时间正好是消息索引记录写入的时间 offset = targetOffset; } else { if (leftIndexValue == -1) { // timestamp 时间小于该MapedFile中第一条记录记录的时间 offset = rightOffset; } else if (rightIndexValue == -1) { // timestamp 时间大于该MapedFile中最后一条记录记录的时间 offset = leftOffset; } else { // 取最接近timestamp的offset offset = Math.abs(timestamp - leftIndexValue) > Math.abs(timestamp - rightIndexValue) ? rightOffset : leftOffset; } } return (mapedFile.getFileFromOffset() + offset) / CQStoreUnitSize; } finally { sbr.release(); } } } // 映射文件被标记为不可用时返回0 return 0; } /** * 根据物理Offset删除无效逻辑文件 */ public void truncateDirtyLogicFiles(long phyOffet) { /** * chen.si phyOffset是最大commit log offset,也就是commit log下一个待写消息的位置,基于这个offset,将多余的文件删除掉 */ // 逻辑队列每个文件大小 int logicFileSize = this.mapedFileSize; // 先改变逻辑队列存储的物理Offset /** * chen.si TODO 暂时不理解这里的目的 * * 再次看了一下,还是无法理解其用意,本身这个值并未在下面的逻辑中读,只有写。 * 在任何异常情况下,也还没理解需要-1。 * * 2017/04/13 * 这里的maxPhysicOffset本质上是用来表示 consume queue中的最后一个逻辑消息对应的物理消息的physical offset * * 而参数传递进来的phyOffset,是commit log的下一个待写的位置,示意图如下: * * |msg1|msg2|msg3|msg4|...... * ^ ^ * maxPhysicOffset是第1个^的offset,指向commit log最后一条消息的起始offset * phyOffset 是第2个^的offset,指向commit log下一个消息的待写offset * * maxPhysicOffset指向的是有效的位置;而phyOffset指向的是一个无效的位置(意思是说没有实际的消息数据) * 为了保证maxPhysicOffset语义上的一致性,因此将maxPhysicOffset设置为phyOffset-1,指向最后一条物理消息的末尾 * 至少是有效的,指向实际的消息数据。而且后续的恢复中,只要有一个有效消息,立刻就会将maxPhysicOffset设置为指向新的物理消息的起始offset * * 2017/05/08 * 这里是一个偷懒的做法,或者说是个无奈的做法。准确来说,应该是找到最后一条物理消息的起始offset。但是因为设计问题, * 导致无法找到最后一条消息的起始offset,只能将就,找到最后一条物理消息的末尾有效字节 */ this.maxPhysicOffset = phyOffet - 1; while (true) { MapedFile mapedFile = this.mapedFileQueue.getLastMapedFile2(); if (mapedFile != null) { ByteBuffer byteBuffer = mapedFile.sliceByteBuffer(); // 先将Offset清空 mapedFile.setWrotePostion(0); mapedFile.setCommittedPosition(0); for (int i = 0; i < logicFileSize; i += CQStoreUnitSize) { long offset = byteBuffer.getLong(); int size = byteBuffer.getInt(); byteBuffer.getLong(); // 逻辑文件起始单元 if (0 == i) { /** * chen.si 0 == i的判断,是因为 存在 删除这个文件 的可能性 */ /** * chen.si 索引文件内的第1个索引消息对应的phy offset 大于等于 传入的offset,所以直接删除掉该文件 */ if (offset >= phyOffet) { this.mapedFileQueue.deleteLastMapedFile(); break; } else { /** * chen.si phy offset符合条件,因此设置 相关参数 */ int pos = i + CQStoreUnitSize; mapedFile.setWrotePostion(pos); mapedFile.setCommittedPosition(pos); this.maxPhysicOffset = offset; } } // 逻辑文件中间单元 else { // 说明当前存储单元有效 if (offset >= 0 && size > 0) { // 如果逻辑队列存储的最大物理offset大于物理队列最大offset,则返回 /** * chen.si 不清理掉这种多余的消息? * * --即使设置了wrote pos 和 comit pos,后续写会覆盖消息,但是如果立刻正常关闭,又会如何 */ if (offset >= phyOffet) { return; } int pos = i + CQStoreUnitSize; mapedFile.setWrotePostion(pos); mapedFile.setCommittedPosition(pos); this.maxPhysicOffset = offset; // 如果最后一个MapedFile扫描完,则返回 if (pos == logicFileSize) { return; } } else { /** * chen.si 到了该文件的最后一条消息 */ return; } } } } else { break; } } } /** * 返回最后一条消息对应物理队列的Next Offset * * chen.si 这个方法不可靠。 commit log是全局的,单从一个cq就推断出下一个 commitlog的位置,有问题。 不过没有找到 使用这个方法 的地方 */ public long getLastOffset() { // 物理队列Offset long lastOffset = -1; // 逻辑队列每个文件大小 int logicFileSize = this.mapedFileSize; MapedFile mapedFile = this.mapedFileQueue.getLastMapedFile2(); if (mapedFile != null) { ByteBuffer byteBuffer = mapedFile.sliceByteBuffer(); // 先将Offset清空 mapedFile.setWrotePostion(0); mapedFile.setCommittedPosition(0); for (int i = 0; i < logicFileSize; i += CQStoreUnitSize) { long offset = byteBuffer.getLong(); int size = byteBuffer.getInt(); byteBuffer.getLong(); // 说明当前存储单元有效 if (offset >= 0 && size > 0) { lastOffset = offset + size; int pos = i + CQStoreUnitSize; mapedFile.setWrotePostion(pos); mapedFile.setCommittedPosition(pos); this.maxPhysicOffset = offset; } else { break; } } } return lastOffset; } public boolean commit(final int flushLeastPages) { return this.mapedFileQueue.commit(flushLeastPages); } public int deleteExpiredFile(long offset) { /** * chen.si 传入的offset是commit log的最小offset */ int cnt = this.mapedFileQueue.deleteExpiredFileByOffset(offset, CQStoreUnitSize); // 无论是否删除文件,都需要纠正下最小值,因为有可能物理文件删除了, // 但是逻辑文件一个也删除不了 /** * chen.si 2种情况,会导致minLogicOffset发生变化: * * 1. 索引队列头 对应的几个文件,被删除掉 * * 2. 物理文件(commit log)被删除掉了,但是 索引文件 没删除掉。 为了保证有效性,同样需要correct */ this.correctMinOffset(offset); return cnt; } /** * 逻辑队列的最小Offset要比传入的物理最小phyMinOffset大 */ public void correctMinOffset(long phyMinOffset) { MapedFile mapedFile = this.mapedFileQueue.getFirstMapedFileOnLock(); if (mapedFile != null) { SelectMapedBufferResult result = mapedFile.selectMapedBuffer(0); if (result != null) { try { // 有消息存在 for (int i = 0; i < result.getSize(); i += ConsumeQueue.CQStoreUnitSize) { long offsetPy = result.getByteBuffer().getLong(); result.getByteBuffer().getInt(); result.getByteBuffer().getLong(); /** * 目的: 保证索引队列的有效性(部分头消息已经是无效的,因为commit log被删除) * * 基础:minLogicOffset 标识 索引队列的第1个有效消息 * * 因为commit log被删除了,所以 索引文件中 开始的部分索引消息 已经是 无效的了, 所以调整minLogicOffset,直到有效 */ if (offsetPy >= phyMinOffset) { this.minLogicOffset = result.getMapedFile().getFileFromOffset() + i; log.info("compute logics min offset: " + this.getMinOffsetInQuque() + ", topic: " + this.topic + ", queueId: " + this.queueId); break; } } } catch (Exception e) { e.printStackTrace(); } finally { result.release(); } } } } public long getMinOffsetInQuque() { return this.minLogicOffset / CQStoreUnitSize; } public void putMessagePostionInfoWrapper(long offset, int size, long tagsCode, long storeTimestamp, long logicOffset) { final int MaxRetries = 5; boolean canWrite = this.defaultMessageStore.getRunningFlags().isWriteable(); for (int i = 0; i < MaxRetries && canWrite; i++) { /** * chen.si 存储索引消息 */ boolean result = this.putMessagePostionInfo(offset, size, tagsCode, logicOffset); if (result) { /** * chen.si 设置checkpoint * * 恢复流程 和 正常接收流程 都会更新 checkpoint */ this.defaultMessageStore.getStoreCheckpoint().setLogicsMsgTimestamp(storeTimestamp); return; } // 只有一种情况会失败,创建新的MapedFile时报错或者超时 else { log.warn("put commit log postion info to " + topic + ":" + queueId + " " + offset + " failed, retry " + i + " times"); try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { log.warn("", e); } } } this.defaultMessageStore.getRunningFlags().makeLogicsQueueError(); } /** * 存储一个20字节的信息,putMessagePostionInfo只有一个线程调用,所以不需要加锁 * * chen.si: * * 1. 存储索引消息 * 2. 调整潜在minlogicoffset * 3. 更新maxPhyOffset * * @param offset * 消息对应的CommitLog offset * @param size * 消息在CommitLog存储的大小 * @param tagsCode * tags 计算出来的长整数 * @return 是否成功 */ private boolean putMessagePostionInfo(final long offset, final int size, final long tagsCode, final long cqOffset) { // 在数据恢复时会走到这个流程 /** * chen.si 这里是恢复的关键点,cq中的消息 有2种情况需要考虑: * * 1. commit log中的物理消息存储成功,对应的cq的消息也存储成功, * 2. commit log中的物理消息存储成功,对应的cq的消息 未 存储成功(可能原因: broker被强制关闭等) * * 对于第1种情况,不需要执行cq的写入操作,所以直接返回 * 对于第2种情况,需要执行cq的写入操作,以 补偿 commit log中 未写入cq 的消息 * * 这里的判断依据,就是依赖 commit log中消息的phy offset 以及 cq对应的最后一条索引消息对应的 commitlog中的 phy offset * * 如果offset <= this.maxPhysicOffset, 说明 消息在cq中是存在的 * * 否则的话,消息在cq不存在,执行写入操作。 * * 那如果中间的消息丢失,怎么办? 不可能,commit log是同步操作的,消息是按照顺序在commit log中存储的,所以也是一条条触发给cq的 */ /** * chen.si 恢复流程走到这里,说明消息已经在cq存储成功,不需要 恢复写入,直接返回。 * * 2017/04/13 异常流程下,根据checkpoint恢复时,会根据这里的物理消息的physicOffset判断,是否在逻辑队列中已经存在 * 这里是否存在的判断,完全是根据 offset的大小进行判断的, * * 正常接收消息写入,不会走到里面 */ if (offset <= this.maxPhysicOffset) { return true; } /** * chen.si 恢复时,如果commit log消息 未 来得及写入 cq,这里会 写 * * 正常接收消息写入,也会走这里 */ this.byteBufferIndex.flip(); this.byteBufferIndex.limit(CQStoreUnitSize); this.byteBufferIndex.putLong(offset); this.byteBufferIndex.putInt(size); this.byteBufferIndex.putLong(tagsCode); final long realLogicOffset = cqOffset * CQStoreUnitSize; MapedFile mapedFile = this.mapedFileQueue.getLastMapedFile(realLogicOffset); if (mapedFile != null) { // 纠正MapedFile逻辑队列索引顺序 /** * chen.si 所谓的逻辑队列索引顺序,比较难理解,参考如下2点: * 1. cq中的消息是固定的20字节,所以 第几条消息在cq的位置中是固定的 比如:第1条消息,是:0-20字节的位置;第2条消息,是:20-40字节的位置;第N条消息,是:(N-1)*20 - N*20 2. 这里是假设: cq中即将写入的消息偏移(即:第几条消息) 与 当前cq文件的待写入 位置 不匹配 所以认为是索引顺序有错误,进行调整,方法为:使用特殊的20字节进行填充文件开始部分,以修改cq文件的待写入位置。这里的前提是: 消息偏移 的 期望位置 比 当前位置 要小。 2017/04/13 这里发现,所谓的调整,实际上并不是针对次序错误的调整。这里的逻辑的一个基础原则就是:只要cq中有文件,就应该是正确的。 唯一需要调整的是,如果cq是空的(可能是过期删除掉的),则重建cq,而commit log中对应的消息很可能 不是 cq中的第1个消息, 此时则需要在新的cq文件中,直接将消息写到期望的位置,而该位置之前的内容,全部用特殊的填充消息写入 */ if (mapedFile.isFirstCreateInQueue() && cqOffset != 0 && mapedFile.getWrotePostion() == 0) { /** * chen.si 符合条件: * mapedFile.isFirstCreateInQueue() 当前file queue的第1个文件 * cqOffset != 0 消息在filequeue中的offset不是0,也就是说 不是系统的第1条消息 * mapedFile.getWrotePostion() == 0 当前file创建后的第1次 写 */ /** * chen.si 设置当前在写文件的初始消息的real logic offset */ this.minLogicOffset = realLogicOffset; /** * chen.si 根据逻辑计算,找到realLogicOffset在当前文件中的实际位置。 将文件开始 到 实际位置 之间,用 特殊的20字节填充提升 wrote position */ this.fillPreBlank(mapedFile, realLogicOffset); log.info("fill pre blank space " + mapedFile.getFileName() + " " + realLogicOffset + " " + mapedFile.getWrotePostion()); } if (cqOffset != 0) { /** * chen.si 尽管调整了,但是仍然存在 逻辑队列索引顺序问题。 比如 realLogicOffset滞后。此时只能告警 * * 2017/04/13 这里其实不是调整,参考之前说的。这里的严重告警是说,发现commit log中记录的queue offset与 实际的consume queue的待写入位置不一致。 * * 认为是一个严重的bug,原因未明。 * * 而且后续的处理逻辑,是将错就错,继续将消息写入append到末尾。实际上这里会永远都不一致了。 * * 2017/04/14 */ if (realLogicOffset != (mapedFile.getWrotePostion() + mapedFile.getFileFromOffset())) { log.warn("logic queue order maybe wrong " + realLogicOffset + " " + (mapedFile.getWrotePostion() + mapedFile.getFileFromOffset())); } } /** * chen.si 每次存储新消息,都更新maxPhysicOffset,也就是cq中对应commit log中的最大offset */ // 记录物理队列最大offset this.maxPhysicOffset = offset; return mapedFile.appendMessage(this.byteBufferIndex.array()); } return false; } private void fillPreBlank(final MapedFile mapedFile, final long untilWhere) { ByteBuffer byteBuffer = ByteBuffer.allocate(CQStoreUnitSize); byteBuffer.putLong(0L); byteBuffer.putInt(Integer.MAX_VALUE); byteBuffer.putLong(0L); int until = (int) (untilWhere % this.mapedFileQueue.getMapedFileSize()); for (int i = 0; i < until; i += CQStoreUnitSize) { mapedFile.appendMessage(byteBuffer.array()); } } /** * 返回Index Buffer * * @param startIndex * 起始偏移量索引 */ public SelectMapedBufferResult getIndexBuffer(final long startIndex) { int mapedFileSize = this.mapedFileSize; long offset = startIndex * CQStoreUnitSize; MapedFile mapedFile = this.mapedFileQueue.findMapedFileByOffset(offset); if (mapedFile != null) { SelectMapedBufferResult result = mapedFile.selectMapedBuffer((int) (offset % mapedFileSize)); return result; } return null; } /** * chen.si 跳过 逻辑索引index 对应的文件,跳转到下一个文件,返回下一个文件的起始offset * * @param index * @return */ public long rollNextFile(final long index) { int mapedFileSize = this.mapedFileSize; int totalUnitsInFile = mapedFileSize / CQStoreUnitSize; return (index + totalUnitsInFile - index % totalUnitsInFile); } public String getTopic() { return topic; } public int getQueueId() { return queueId; } public long getMaxPhysicOffset() { return maxPhysicOffset; } public void setMaxPhysicOffset(long maxPhysicOffset) { this.maxPhysicOffset = maxPhysicOffset; } public void destroy() { this.maxPhysicOffset = -1; this.minLogicOffset = 0; this.mapedFileQueue.destroy(); } public long getMinLogicOffset() { return minLogicOffset; } public void setMinLogicOffset(long minLogicOffset) { this.minLogicOffset = minLogicOffset; } /** * 获取当前队列中的消息总数 */ public long getMessageTotalInQueue() { return this.getMaxOffsetInQuque() - this.getMinOffsetInQuque(); } public long getMaxOffsetInQuque() { return this.mapedFileQueue.getMaxOffset() / CQStoreUnitSize; } }