深度解析RocketMq源码-持久化组件(二) MappedFileQueue

1.绪论

MappedFileQueue是commitLog中最核心的主组件。前面讲解commitLog的时候也曾说过,MappedFileQueue本质上就是一个MappedFile队列,而commitLog操纵Mmapped读写的时候,也是通过MappedFileQueue来实现的。

commitlog和mappedfilequeue和mappedfile的关系如图所示:

所以我们需要分析一下mappedfilequeue的具体作用。

2.MappedFileQueue

2.1 组成

public class MappedFileQueue implements Swappable {
    private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
    private static final Logger LOG_ERROR = LoggerFactory.getLogger(LoggerName.STORE_ERROR_LOGGER_NAME);

    protected final String storePath;
    //mappedfile的大小
    protected final int mappedFileSize;
    //mappedfile的队列
    protected final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<>();
    //mappedFile分配服务
    protected final AllocateMappedFileService allocateMappedFileService;
    //刷新和commit的指针
    protected long flushedWhere = 0;
    protected long committedWhere = 0;
    //存储时间戳,主要是删除过期的mappedfile使用
    protected volatile long storeTimestamp = 0;
}

2.2 检查每个commitfile是否损坏

其实就是遍历前n-1个commitfile的大小,判断是否为mappedFileSize,如果不是,便认为文件已经损坏。

//检查每个commitfile是否损坏
    public void checkSelf() {
        List<MappedFile> mappedFiles = new ArrayList<>(this.mappedFiles);
        if (!mappedFiles.isEmpty()) {
            Iterator<MappedFile> iterator = mappedFiles.iterator();
            MappedFile pre = null;
            //遍历每个mappedfile
            while (iterator.hasNext()) {
                MappedFile cur = iterator.next();
                    
                if (pre != null) {
                    //计算出每个commitfile的大小看是否为mappedFileSize(1gb)
                    if (cur.getFileFromOffset() - pre.getFileFromOffset() != this.mappedFileSize) {
                        LOG_ERROR.error("[BUG]The mappedFile queue's data is damaged, the adjacent mappedFile's offset don't match. pre file {}, cur file {}",
                            pre.getFileName(), cur.getFileName());
                    }
                }
                pre = cur;
            }
        }
    }

2.2 根据时间戳获取大于该时间戳或者小于时间戳的消息的时间

可以看出mappedfile中的startTimestamp为第一条消息的写入时间,stopTimestamp为最后一条消息的写入时间。并且该方法根据boundaryType进行判断:

如果boundaryType传参为LOWER,表示获取最后一条消息写入时间大于等于传入时间的mappedfile;如果boundaryType传参为UPPER,表示获取第一条消息写入时间小于等于传入时间的mappedfile
  //根据时间戳,获取一段数据
    public MappedFile getConsumeQueueMappedFileByTime(final long timestamp, CommitLog commitLog,
        BoundaryType boundaryType) {
        //获取commitfile数组
        Object[] mfs = copyMappedFiles(0);
        if (null == mfs) {
            return null;
        }

        //从后往前遍历m每个mappedfile
        for (int i = mfs.length - 1; i >= 0; i--) {
            DefaultMappedFile mappedFile = (DefaultMappedFile) mfs[i];
            // Figure out the earliest message store time in the consume queue mapped file.
            if (mappedFile.getStartTimestamp() < 0) {
                //从commitlog的持久化文件中获取到该commitlog开始写入数据的时间戳并且设置到commitlogfile中
                SelectMappedBufferResult selectMappedBufferResult = mappedFile.selectMappedBuffer(0, ConsumeQueue.CQ_STORE_UNIT_SIZE);
                if (null != selectMappedBufferResult) {
                    try {
                        ByteBuffer buffer = selectMappedBufferResult.getByteBuffer();
                        long physicalOffset = buffer.getLong();
                        int messageSize = buffer.getInt();
                        long messageStoreTime = commitLog.pickupStoreTimestamp(physicalOffset, messageSize);
                        if (messageStoreTime > 0) {
                            mappedFile.setStartTimestamp(messageStoreTime);
                        }
                    } finally {
                        selectMappedBufferResult.release();
                    }
                }
            }
            // Figure out the latest message store time in the consume queue mapped file.
            //从commitlog的持久化文件中获取到该commitlog最后一条消息的写入的时间戳并且设置到commitlogfile中
            if (i < mfs.length - 1 && mappedFile.getStopTimestamp() < 0) {
                SelectMappedBufferResult selectMappedBufferResult = mappedFile.selectMappedBuffer(mappedFileSize - ConsumeQueue.CQ_STORE_UNIT_SIZE, ConsumeQueue.CQ_STORE_UNIT_SIZE);
                if (null != selectMappedBufferResult) {
                    try {
                        ByteBuffer buffer = selectMappedBufferResult.getByteBuffer();
                        long physicalOffset = buffer.getLong();
                        int messageSize = buffer.getInt();
                        long messageStoreTime = commitLog.pickupStoreTimestamp(physicalOffset, messageSize);
                        if (messageStoreTime > 0) {
                            mappedFile.setStopTimestamp(messageStoreTime);
                        }
                    } finally {
                        selectMappedBufferResult.release();
                    }
                }
            }
        }
        
        
        switch (boundaryType) {
            //如果boundaryType传参为LOWER,表示获取最后一条消息写入时间大于等于传入时间的mappedfile
            case LOWER: {
                for (int i = 0; i < mfs.length; i++) {
                    DefaultMappedFile mappedFile = (DefaultMappedFile) mfs[i];
                    if (i < mfs.length - 1) {
                        long stopTimestamp = mappedFile.getStopTimestamp();
                        if (stopTimestamp >= timestamp) {
                            return mappedFile;
                        }
                    }

                    // Just return the latest one.
                    if (i == mfs.length - 1) {
                        return mappedFile;
                    }
                }
            }
            //如果boundaryType传参为UPPER,表示获取第一条消息写入时间小于等于传入时间的mappedfile
            case UPPER: {
                for (int i = mfs.length - 1; i >= 0; i--) {
                    DefaultMappedFile mappedFile = (DefaultMappedFile) mfs[i];
                    if (mappedFile.getStartTimestamp() <= timestamp) {
                        return mappedFile;
                    }
                }
            }

           
        }
        return null;
    }

2.3 获取修改时间大于等于传入时间的mappedfile

    //获取修改时间大于等于传入时间的mappedfile
    public MappedFile getMappedFileByTime(final long timestamp) {
        Object[] mfs = this.copyMappedFiles(0);

        if (null == mfs)
            return null;

        for (int i = 0; i < mfs.length; i++) {
            MappedFile mappedFile = (MappedFile) mfs[i];
            if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
                return mappedFile;
            }
        }

        return (MappedFile) mfs[mfs.length - 1];
    }

2.4 根据offset,删除大于传入offset后面的所有数据

    //根据offset,删除大于传入offset后面的所有数据
    public void truncateDirtyFiles(long offset) {
        List<MappedFile> willRemoveFiles = new ArrayList<>();

        for (MappedFile file : this.mappedFiles) {
            long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize;
            if (fileTailOffset > offset) {
                //如果offset在对应的mappedfile中,需要重置写指针、flsuh指针和commit指针
                if (offset >= file.getFileFromOffset()) {
                    file.setWrotePosition((int) (offset % this.mappedFileSize));
                    file.setCommittedPosition((int) (offset % this.mappedFileSize));
                    file.setFlushedPosition((int) (offset % this.mappedFileSize));
                } else {
                    //删除小于offset的mappedfile,主要包括关闭niochannel,删除磁盘文件和利用buffer的clean方法清除buffer
                    file.destroy(1000);
                    willRemoveFiles.add(file);
                }
            }
        }

        this.deleteExpiredFile(willRemoveFiles);
    }

2.5 从mappedfilequeue中删除对应的文件

   //从mappedfilequeu中删除过期的文件(传入的files)
    void deleteExpiredFile(List<MappedFile> files) {

        if (!files.isEmpty()) {

            Iterator<MappedFile> iterator = files.iterator();
            while (iterator.hasNext()) {
                MappedFile cur = iterator.next();
                if (!this.mappedFiles.contains(cur)) {
                    iterator.remove();
                    log.info("This mappedFile {} is not contained by mappedFiles, so skip it.", cur.getFileName());
                }
            }

            try {
                if (!this.mappedFiles.removeAll(files)) {
                    log.error("deleteExpiredFile remove failed.");
                }
            } catch (Exception e) {
                log.error("deleteExpiredFile has exception.", e);
            }
        }
    }

2.6 broker初始化的时候,如何将磁盘文件与内存建立映射关系

其实在构建mappedfile的时候,会调用mappedfile的init方法,该方法会见了mappedByteBuffer和磁盘文件的映射关系。

  //在broker重启时,需要从磁盘中将mappedfile读取到内存中
    public boolean doLoad(List<File> files) {
        
        //根据名称排序,mappedfile的名称其实就是文件的物理偏移量
        files.sort(Comparator.comparing(File::getName));

        for (int i = 0; i < files.size(); i++) {
            File file = files.get(i);
            if (file.isDirectory()) {
                continue;
            }

            if (file.length() == 0 && i == files.size() - 1) {
                boolean ok = file.delete();
                log.warn("{} size is 0, auto delete. is_ok: {}", file, ok);
                continue;
            }

            if (file.length() != this.mappedFileSize) {
                log.warn(file + "\t" + file.length()
                        + " length not matched message store config value, please check it manually");
                return false;
            }

            try {
                //新建一个mappedfile,这里会调用mappedfile的init方法,他会新建一个mappedbytebuffer,并且与文件建立映射关系
                MappedFile mappedFile = new DefaultMappedFile(file.getPath(), mappedFileSize);
                //初始化文件的写指针、flush指针、commit指针
                mappedFile.setWrotePosition(this.mappedFileSize);
                mappedFile.setFlushedPosition(this.mappedFileSize);
                mappedFile.setCommittedPosition(this.mappedFileSize);
                //加入到mappedfilequeue中
                this.mappedFiles.add(mappedFile);
                log.info("load " + file.getPath() + " OK");
            } catch (IOException e) {
                log.error("load file " + file + " error", e);
                return false;
            }
        }
        return true;
    }

2.7 根据起始偏移量获取到最后一块mappedfile

在获取到最后一块mappedfile的时候,可以根据needcreate参数判断:如果超过内存范围,是否需要重新构建一个mappedfile。

   //重要,根据起始偏移量获取到最后一块mappedfile,并且根据needcreate参数判断如果超过内存范围,是否需要重新构建一个mappedfile
    public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
        long createOffset = -1;
        //获取到mappedfilequeue中的最后一个mappedfile
        MappedFile mappedFileLast = getLastMappedFile();

        //如果最后一块mappedfile为空,表示此时为初始化,获取到新的mapperfile的起始位置
        if (mappedFileLast == null) {
            createOffset = startOffset - (startOffset % this.mappedFileSize);
        }
        //如果不为空,但是最后一块mappedfile已经满了,也需要新建一块mappedfile
        if (mappedFileLast != null && mappedFileLast.isFull()) {
            createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
        }

        if (createOffset != -1 && needCreate) {
            //新建mappedfile
            return tryCreateMappedFile(createOffset);
        }

        return mappedFileLast;
    }

根据代码可以看出,mappedfile其实是在tryCreateMappedFile这个方法中创建出来的。我们仔细研究一下mappedfile的创建。

2.8 mappedfile的创建

2.8.1 mappedfile的创建分析

创建其实分成两种形式,一是同步创建,即直接调用new方法创建mappedfile文件,另一种是调用allocateMappedFileService进行异步创建。

 public MappedFile tryCreateMappedFile(long createOffset) {
        String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
        String nextNextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset
                + this.mappedFileSize);
        return doCreateMappedFile(nextFilePath, nextNextFilePath);
    }

    protected MappedFile doCreateMappedFile(String nextFilePath, String nextNextFilePath) {
        MappedFile mappedFile = null;
        //创建包含两种形式:一是直接创建 二是交给allocateMappedFileService进行异步创建。
        if (this.allocateMappedFileService != null) {
            //可以看出本质上还是调用的allocateMappedFileService的putRequestAndReturnMappedFile方法来进行创建的,并且创建的时候会
            //创建两个mappedfile,其实预热的思想,在mappedfile的基础上会创建两个mappedfile文件
            mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
                    nextNextFilePath, this.mappedFileSize);
        } else {
            try {
                mappedFile = new DefaultMappedFile(nextFilePath, this.mappedFileSize);
            } catch (IOException e) {
                log.error("create mappedFile exception", e);
            }
        }

        if (mappedFile != null) {
            if (this.mappedFiles.isEmpty()) {
                mappedFile.setFirstCreateInQueue(true);
            }
            //将创建结果加入到mqppedqueu中
            this.mappedFiles.add(mappedFile);
        }

        return mappedFile;
    }

2.8.2 mappedfile异步创建服务-AllocateMappedFileService

其核心就是将分配请求加入到一个队列中,然后启动线程来进行消费并且分配mappedfile。

1.基本组成
public class AllocateMappedFileService extends ServiceThread {
    private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
    private static int waitTimeOut = 1000 * 5;
    //存储mappedfile分配请求的queue
    private ConcurrentMap<String, AllocateRequest> requestTable =
        new ConcurrentHashMap<>();
    private PriorityBlockingQueue<AllocateRequest> requestQueue =
        new PriorityBlockingQueue<>();
    private volatile boolean hasException = false;
    private DefaultMessageStore messageStore;
}
2.如何进行分配的
   public void run() {
        log.info(this.getServiceName() + " service started");

        while (!this.isStopped() && this.mmapOperation()) {

        }
        log.info(this.getServiceName() + " service end");
    }
  private boolean mmapOperation() {
        boolean isSuccess = false;
        AllocateRequest req = null;
        try {
            //取出分配mappedfile的请求
            req = this.requestQueue.take();
            AllocateRequest expectedRequest = this.requestTable.get(req.getFilePath());
            if (null == expectedRequest) {
                log.warn("this mmap request expired, maybe cause timeout " + req.getFilePath() + " "
                    + req.getFileSize());
                return true;
            }
            if (expectedRequest != req) {
                log.warn("never expected here,  maybe cause timeout " + req.getFilePath() + " "
                    + req.getFileSize() + ", req:" + req + ", expectedRequest:" + expectedRequest);
                return true;
            }

            if (req.getMappedFile() == null) {
                long beginTime = System.currentTimeMillis();

                MappedFile mappedFile;
                //判断是否开启了瞬时存储技术
                if (messageStore.isTransientStorePoolEnable()) {
                    try {
                        mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
                        //如果开启了瞬时存储技术,调用mappedflile中支持瞬时存储技术的init方法
                        mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
                    } catch (RuntimeException e) {
                        log.warn("Use default implementation.");
                        mappedFile = new DefaultMappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
                    }
                } else {
                    //如果没有开启,便调用mappedflile中不支持瞬时存储技术的init方法
                    mappedFile = new DefaultMappedFile(req.getFilePath(), req.getFileSize());
                }

                long elapsedTime = UtilAll.computeElapsedTimeMilliseconds(beginTime);
                if (elapsedTime > 10) {
                    int queueSize = this.requestQueue.size();
                    log.warn("create mappedFile spent time(ms) " + elapsedTime + " queue size " + queueSize
                        + " " + req.getFilePath() + " " + req.getFileSize());
                }

                // pre write mappedFile
                //进行mappedfile的预热
                if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig()
                    .getMappedFileSizeCommitLog()
                    &&
                    this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
                    mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
                        this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
                }

                req.setMappedFile(mappedFile);
                this.hasException = false;
                isSuccess = true;
            }
    }
}

可以看出AllocateMappedFileService本质上就是调用mappedfile的init方法,来对mappedfile的构造和初始化。

2.9 flush mappedfile

即找到上次flsuh的位置,调用mappedfile的flush方法,将mappedbytebuffer中的文件flush到磁盘中去。

    //将mappedfile flush到磁盘中去
    public boolean flush(final int flushLeastPages) {
        
        boolean result = true;
        //找到上一次flush的位置
        MappedFile mappedFile = this.findMappedFileByOffset(this.getFlushedWhere(), this.getFlushedWhere() == 0);
        if (mappedFile != null) {
            long tmpTimeStamp = mappedFile.getStoreTimestamp();
            //调用flush方法刷新磁盘文件
            int offset = mappedFile.flush(flushLeastPages);
            long where = mappedFile.getFileFromOffset() + offset;
            result = where == this.getFlushedWhere();
            this.setFlushedWhere(where);
            if (0 == flushLeastPages) {
                this.setStoreTimestamp(tmpTimeStamp);
            }
        }

        return result;
    }

2.10 commit mappedfile

 public synchronized boolean commit(final int commitLeastPages) {
        boolean result = true;
        //找到上一次commit的位置
        MappedFile mappedFile = this.findMappedFileByOffset(this.getCommittedWhere(), this.getCommittedWhere() == 0);
        if (mappedFile != null) {
            //调用mappedfile的commit方法commit
            int offset = mappedFile.commit(commitLeastPages);
            long where = mappedFile.getFileFromOffset() + offset;
            result = where == this.getCommittedWhere();
            this.setCommittedWhere(where);
        }

        return result;
    }

2.11 根据offset返回所在的mappedfile

//返回对应offset在的那个mappedfile
    public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
        try {
            MappedFile firstMappedFile = this.getFirstMappedFile();
            MappedFile lastMappedFile = this.getLastMappedFile();
            if (firstMappedFile != null && lastMappedFile != null) {
                if (offset < firstMappedFile.getFileFromOffset() || offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) {
                    LOG_ERROR.warn("Offset not matched. Request offset: {}, firstOffset: {}, lastOffset: {}, mappedFileSize: {}, mappedFiles count: {}",
                        offset,
                        firstMappedFile.getFileFromOffset(),
                        lastMappedFile.getFileFromOffset() + this.mappedFileSize,
                        this.mappedFileSize,
                        this.mappedFiles.size());
                } else {
                    int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
                    MappedFile targetFile = null;
                    try {
                        targetFile = this.mappedFiles.get(index);
                    } catch (Exception ignored) {
                    }

                    if (targetFile != null && offset >= targetFile.getFileFromOffset()
                        && offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
                        return targetFile;
                    }

                    for (MappedFile tmpMappedFile : this.mappedFiles) {
                        if (offset >= tmpMappedFile.getFileFromOffset()
                            && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
                            return tmpMappedFile;
                        }
                    }
                }

                if (returnFirstOnNotFound) {
                    return firstMappedFile;
                }
            }
        } catch (Exception e) {
            log.error("findMappedFileByOffset Exception", e);
        }

        return null;
    }

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值