RocketMQ源碼分析 broker啓動,commitlog、consumequeue、indexfile、MappedFileQueue、MappedFile之間的關係以及位置說明

1.MappedFile類屬性說明

dubbo的核心是spi,看懂了spi那麼duboo基本上也懂了,對於rmq來說,它的核心是broker,而broker的核心是commitlog、consumequeue、indexfile,而這些文件對應的最終都是MappedFile,那麼搞明白了這個類,那麼對於broker的存儲這塊也就很容易明白了

1.1.MappedFile類屬性如下

OS_PAGE_SIZE是4k,表示操作系統頁

TOTAL_MAPPED_VIRTUAL_MEMORY,TOTAL_MAPPED_FILES 都是static變量,分別保存的是總共映射的數據容量,映射的總文件數

wrotePosition 表示當前寫的位置,具體啥是當前寫的位置,後續分析

committedPosition 表示當前提交的位置,具體咋理解,往後看

flushedPosition 表示當前刷新的位置,具體咋理解,往後看,以上這三個變量很重要

fileSize 表示該MappedFile對應的磁盤文件的size,對於commitlog來說是1G,對於consumequeue來說是6000000 字節,對於indexfile來說是42040字節

fileChannel表示該文件的通道

writeBuffer是堆外內存,在開啓了transientStorePoolEnable=true的情況下非null,生產通常都會開啓transientStorePoolEnable以供消費時候讀寫分離

transientStorePool是堆外內存池,在開啓了transientStorePool的情況有有值

fileName 文件名,比如對於commitlog來說第一個文件是0*1024^3,第二個文件是1*1024^3

fileFromOffset 即文件的起始位置(相對於整個commitlog or consumequeue來說),比如commitlog consumequeue文件來說,該值就是fileName

file 就是文件

mappedByteBuffer 即pagecache,通過fileChannel.map生成

storeTimestamp 最後一次消息的存儲時間

firstCreateInQueue 對於隊列有用

1.2.MappedFile構造器說明

有兩個構造器

 MappedFile(final String fileName, final int fileSize)

MappedFile(final String fileName, final int fileSize, final TransientStorePool transientStorePool)

在不開啓transientStorePoolEnable=true的情況下,都是使用第一個構造器

在開啓的情況下,broker啓動進行load操作加載commitlog consumequeue indexfile文件都是使用第一個構造器,在broker運行過程中,consumequeue indexfiel創建也是使用第一個構造器,在broker運行過程中,創建commitlog文件都是使用的第二個構造器,ctrl+alt+H查看調用如下圖

在AllocateMappedFileService.mmapOperation()內通過jdk的spi機制加載META-INF目錄下org.apache.rocketmq.store.MappedFile文件,默認是沒有該文件的,代碼如下

MappedFile mappedFile;
if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
    try {
        mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();//拋出異常,默認META-INF下是沒有org.apache.rocketmq.store.MappedFile文件的
        mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
    } catch (RuntimeException e) {//捕獲異常
        log.warn("Use default implementation.");
        mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());//走這裏
    }
} else {
    mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
}

生產通常是開啓transientStorePoolEnable=true的,那麼以第二個構造器爲例說明,第二個構造器明白了,第一個構造器自然就明白了

public MappedFile(final String fileName, final int fileSize,
    final TransientStorePool transientStorePool) throws IOException {
    init(fileName, fileSize, transientStorePool);
}
public void init(final String fileName, final int fileSize,
    final TransientStorePool transientStorePool) throws IOException {
    init(fileName, fileSize);
    this.writeBuffer = transientStorePool.borrowBuffer();//從堆外內存緩衝池構建堆外內存
    this.transientStorePool = transientStorePool;
}
private void init(final String fileName, final int fileSize) throws IOException {
    this.fileName = fileName;//文件名
    this.fileSize = fileSize;//文件大小
    this.file = new File(fileName);//根據文件構造File
    this.fileFromOffset = Long.parseLong(this.file.getName());//文件名作爲起始offset
    boolean ok = false;

    ensureDirOK(this.file.getParent());

    try {
        this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
        this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);// 將此通道的文件區域直接映射到內存中,該屬性就是pagecache
        TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
        TOTAL_MAPPED_FILES.incrementAndGet();
        ok = true;
    } catch (FileNotFoundException e) {
        log.error("create file channel " + this.fileName + " Failed. ", e);
        throw e;
    } catch (IOException e) {
        log.error("map file " + this.fileName + " Failed. ", e);
        throw e;
    } finally {
        if (!ok && this.fileChannel != null) {
            this.fileChannel.close();
        }
    }
}

該構造器就是根據文件名路徑構造MappedFile文件,構造堆外內存、pagecache屬性,此時創建後wrotePosition committedPosition flushedPosition三個位置都是0。這兩個構造器的唯一區別就是第一個構造器是不賦值其堆外內存屬性writeBuffer。

這裏記錄下堆外內存池TransientStorePool的來源

BrokerStartup.main(String[]) //broker啓動入口
BrokerStartup.createBrokerController(String[])//創建BrokerController
BrokerController.initialize()//BrokerController初始化
new DefaultMessageStore(MessageStoreConfig, BrokerStatsManager, MessageArrivingListener, BrokerConfig)//創建DefaultMessageStore,代碼如下

public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, final BrokerStatsManager brokerStatsManager,
    //省略其他代碼
    this.transientStorePool = new TransientStorePool(messageStoreConfig);

    if (messageStoreConfig.isTransientStorePoolEnable()) {//默認false,如果broker開啓了transientStorePoolEnable=true,則執行。transient含義是短暫的
        this.transientStorePool.init();
    }
	//省略其他代碼
}
//TransientStorePool
public void init() {
    for (int i = 0; i < poolSize; i++) {
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);//創建堆外內存DirectByteBuffer

        final long address = ((DirectBuffer) byteBuffer).address();
        Pointer pointer = new Pointer(address);
        LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));//在物理內存鎖定1G,使用的jna,如果以後自己項目需要進行鎖定內存,那麼則可以參考這裏

        availableBuffers.offer(byteBuffer);//把申請的堆外內存緩存起來
    }
}

 

2.MappedFileQueue類說明

2.1.屬性說明

storePath 是目錄路徑,對commitlog來說是${rocketmq-store}/commitlog   對consumequeue來說是${rocketmq-store}/consumequeue

mappedFileSize 單個MappedFile文件對應的磁盤文件的size,對commitlog來說是1024^3即1G   對consumequeue來說是30w*20即600w

mappedFiles 即MappedFile集合,是CopyOnWriteArrayList類型,併發集合

allocateMappedFileService 分配MappedFile服務AllocateMappedFileService,是個runnable對象,啓動該服務用於創建commitlog MappedFile對象,對於commitlog來說該屬性是AllocateMappedFileService,對於consumequeue來說該屬性是null

flushedWhere committedWhere 分別表示刷新位置,提交位置,跟MappedFile的三個位置關聯,具體後續說明

storeTimestamp 最後存儲的時間戳

實際MappedFileQueue就是個MappedFile集合,個人認爲叫MappedFileList更貼切,叫MappedFileQueue剛開始看的時候總是容易混淆。

2.2.MappedFileQueue構造器說明

MappedFileQueue構造器調用:

2.2.1.對於commitlog,在broker啓動時候初始化BrokerController的時候創建DefaultMessageStore的時候創建CommitLog對象的時候調用,commitlog對象對應的文件結構如圖

2.2.2.對於consumequeue,這個調用就比較多

2.1.在broker啓動進行load操作的時候加載${rocketmq-store}/consumequeue/$topic/$queueId/$fileName的時候針對每一個topic下的queueId下的文件都創建一個ConsumeQueue

一個ConsumeQueue等同一個MappedFileQueue等同多個MappedFile,爲什麼這麼做呢?因爲一個消息隊列會有多個文件,如下圖


 

2.2.在broker運行中DefaultMessageStore.findConsumeQueue(String, int)根據topic queueId查找ConsumeQueue,如果查找不到則創建ConsumeQueue對象,創建方式2.1

 

3.commitlog文件與MappedFileQueue MappedFile關係

在broker啓動的時候創建commitlog對象並加載load磁盤的commitlog文件,並從正常or異常關閉情況恢復,在運行過程中保存消息

3.1.broker啓動創建commitlog對象,即創建MappedFileQueue對象

3.2.broker啓動加載load consumequeue文件

對於創建consumequeue對象來說,只是創建MappedFileQueue對象,並不創建具體的文件對象MappedFile

 

3.3.broker啓動加載load commitlog文件

org.apache.rocketmq.store.MappedFileQueue.load()方法是加載${rocketmq-store}/commitlog or ${rocketmq-store}/consumequeue目錄下的文件,每個文件對應創建一個MappedFile,新建的MappedFile對象wrotePosition committedPosition flushedPosition屬性都設置爲文件名(起始位置),並把新建的MappedFile對象添加到緩存MappedFileQueue.mappedFiles

 

至此,commitlog對象對應的MappedFileQueue的flushedWhere committedWhere都是0,每個MappedFile對象的wrotePosition committedPosition flushedPosition屬性都是文件名

代碼如下

//org.apache.rocketmq.store.DefaultMessageStore.load()
public boolean load() {
    boolean result = true;

    try {
        boolean lastExitOK = !this.isTempFileExist();//存在abort文件,說明broker上次是異常關閉,因爲broker啓動後會創建abort文件,正常關閉會刪除該文件,啓動時候存在該文件,說明上次是異常關閉
        log.info("last shutdown {}", lastExitOK ? "normally" : "abnormally");//abnormally不正常的

        if (null != scheduleMessageService) {
            result = result && this.scheduleMessageService.load();//加載${rocketmq_home}\store\config/delayOffset.json,該文件保存的是每個延時隊列的消費offset,16個延時級別,16個隊列
        }

        // load Commit Log
        result = result && this.commitLog.load();//加載${rocketmq_home}\store\commitlog下的commitlog數據文件

        // load Consume Queue
        result = result && this.loadConsumeQueue();//加載${rocketmq_home}\store\consumequeue下的消費隊列

        if (result) {
            this.storeCheckpoint =
                new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir()));//加載checkpoint文件,該文件是用於異常關閉恢復,保存的是刷盤位置

            this.indexService.load(lastExitOK);//加載加載${rocketmq_home}\store\index目錄下的文件

            this.recover(lastExitOK);//使用commitLog恢復上次異常/正常關閉的broker

            log.info("load over, and the max phy offset = {}", this.getMaxPhyOffset());
        }
    } catch (Exception e) {
        log.error("load exception", e);
        result = false;
    }

    if (!result) {
        this.allocateMappedFileService.shutdown();
    }

    return result;
}

3.3.1.commitlog加載代碼如下

//org.apache.rocketmq.store.CommitLog.load()
public boolean load() {
    boolean result = this.mappedFileQueue.load();//加載I:\rocketmq\store\commitlog
    log.info("load commit log " + (result ? "OK" : "Failed"));
    return result;
}
//org.apache.rocketmq.store.MappedFileQueue.load()
public boolean load() {
    File dir = new File(this.storePath);//${rocketmq_home}\store\commitlog
    File[] files = dir.listFiles();
    if (files != null) {
        // ascending order
        Arrays.sort(files);
        for (File file : files) {

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

            try {
                MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);//爲每個文件創建對應的堆外內存映射

                mappedFile.setWrotePosition(this.mappedFileSize);
                mappedFile.setFlushedPosition(this.mappedFileSize);
                mappedFile.setCommittedPosition(this.mappedFileSize);
                this.mappedFiles.add(mappedFile);
                log.info("load " + file.getPath() + " OK");
            } catch (IOException e) {
                log.error("load file " + file + " error", e);
                return false;
            }
        }
    }

    return true;
}

 最終加載的commitlog文件保存到了CommitLog.mappedFileQueue.mappedFiles集合中。而commitlog對象又被包含在DefaultMessageStore,DefaultMessageStore又被包含在BrokerController對象內,最終在broker啓動加載commitlog文件就被加載到了broker上。

 3.3.2.consumequeue加載如下代碼

//org.apache.rocketmq.store.DefaultMessageStore.loadConsumeQueue()
private boolean loadConsumeQueue() {
    File dirLogic = new File(StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()));//目錄${rocketmq}\store\consumequeue
    File[] fileTopicList = dirLogic.listFiles();//列舉出${rocketmq}\store\consumequeue目錄下的所有文件
    if (fileTopicList != null) {

        for (File fileTopic : fileTopicList) {//遍歷topic目錄文件
            String topic = fileTopic.getName();

            File[] fileQueueIdList = fileTopic.listFiles();
            if (fileQueueIdList != null) {
                for (File fileQueueId : fileQueueIdList) {//遍歷隊列目錄下的文件
                    int queueId;
                    try {
                        queueId = Integer.parseInt(fileQueueId.getName());
                    } catch (NumberFormatException e) {
                        continue;
                    }
                    ConsumeQueue logic = new ConsumeQueue(
                        topic,
                        queueId,
                        StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()),
                        this.getMessageStoreConfig().getMapedFileSizeConsumeQueue(),
                        this);//構建ConsumeQueue,該對象對應MappedFileQueue,對應多個MappedFile
                    this.putConsumeQueue(topic, queueId, logic);
                    if (!logic.load()) {//加載consumequeue文件
                        return false;
                    }
                }
            }
        }
    }

    log.info("load logics queue all over, OK");

    return true;
}
//org.apache.rocketmq.store.ConsumeQueue.load()
public boolean load() {
    boolean result = this.mappedFileQueue.load();
    log.info("load consume queue " + this.topic + "-" + this.queueId + " " + (result ? "OK" : "Failed"));
    if (isExtReadEnable()) {//false
        result &= this.consumeQueueExt.load();
    }
    return result;
}

 最終每個consumequeue被加載到了org.apache.rocketmq.store.DefaultMessageStore.consumeQueueTable集合中保存,key是topic,value是queueID和ConsumeQueue的映射集合

3.3.3.indexfile加載如下

//org.apache.rocketmq.store.index.IndexService.load(boolean)
public boolean load(final boolean lastExitOK) {
    File dir = new File(this.storePath);
    File[] files = dir.listFiles();
    if (files != null) {
        // ascending order
        Arrays.sort(files);
        for (File file : files) {//遍歷${rocketmq_home}\store\index目錄下的indexfile文件
            try {
                IndexFile f = new IndexFile(file.getPath(), this.hashSlotNum, this.indexNum, 0, 0);//吧每個indexfile文件包裝爲IndexFile對象
                f.load();//加載indexfile文件,即把前40字節保存到IndexHeader

                if (!lastExitOK) {//broker上次是異常關閉
                    if (f.getEndTimestamp() > this.defaultMessageStore.getStoreCheckpoint()
                        .getIndexMsgTimestamp()) {//如果indexfile的結束時間戳(保存到indexfile的最後一條消息的時間戳)大於StoreCheckpoint索引刷盤時間戳,則銷燬該indexfile對象,即釋放對應的MappedFile對象(該對象包裝了堆外內存,釋放堆外內存)。 爲什麼要銷燬呢?因爲StoreCheckpoint是刷盤保存點,用於保存commitlog consumequeue indexfile刷盤的位置,便於異常關閉恢復。如果indexfile的結束時間戳大於StoreCheckpoint索引刷盤時間戳,則說明該IndexFile是由於broker異常關閉並沒有被刷盤
                        f.destroy(0);//釋放該MappedFile對象,釋放堆外內存
                        continue;
                    }
                }

                log.info("load index file OK, " + f.getFileName());
                this.indexFileList.add(f);
            } catch (IOException e) {
                log.error("load file {} error", file, e);
                return false;
            } catch (NumberFormatException e) {
                log.error("load file {} error", file, e);
            }
        }
    }

    return true;
}
//org.apache.rocketmq.store.index.IndexFile.load()
public void load() {
    this.indexHeader.load();
}
//org.apache.rocketmq.store.index.IndexHeader.load()
public void load() {
    this.beginTimestamp.set(byteBuffer.getLong(beginTimestampIndex));//獲取indexfile的前8字節,即起始時間戳
    this.endTimestamp.set(byteBuffer.getLong(endTimestampIndex));//結束時間戳 8字節
    this.beginPhyOffset.set(byteBuffer.getLong(beginPhyoffsetIndex));//在commitlog的起始offset 8字節
    this.endPhyOffset.set(byteBuffer.getLong(endPhyoffsetIndex));//在commitlog的結束offset 8字節

    this.hashSlotCount.set(byteBuffer.getInt(hashSlotcountIndex));//已佔用的slot數量 4字節
    this.indexCount.set(byteBuffer.getInt(indexCountIndex));//已經使用的index數量 4字節

    if (this.indexCount.get() <= 0) {
        this.indexCount.set(1);
    }
}

indexfile文件的格式如下

最終每個indexfile被加載到org.apache.rocketmq.store.index.IndexService.indexFileList集合保存,IndexService又包含在DefaultMessageStore。indexfile和上述兩個對象有區別,它只是包裝了MappedFile對象,而Commitlog ConsumeQueue對象都是包裝了MappedFileQueue對象,包裝了MappedFile集合

綜上,broker啓動的時候加載commitlog consumequeue indexfile到broker,關係如圖

3.4.broker啓動恢復consumequeu 和commitlog

分爲broker上次是正常關閉、異常關閉兩種情況

//org.apache.rocketmq.store.DefaultMessageStore.recover(boolean)
private void recover(final boolean lastExitOK) {
    long maxPhyOffsetOfConsumeQueue = this.recoverConsumeQueue();//恢復consumerqueue

    if (lastExitOK) {
        this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);//broker上次是正常關閉
    } else {
        this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);//broker上次是異常關閉
    }

    this.recoverTopicQueueTable();
}

3.4.1.恢復consumerqueue

/*
 * 恢復消費隊列,返回所有消費隊列內的最大offset,即該offset就是commitlog中已經轉儲到消費隊列的offset
 */
//org.apache.rocketmq.store.DefaultMessageStore.recoverConsumeQueue()
private long recoverConsumeQueue() {
    long maxPhysicOffset = -1;
    for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {//遍歷同topic下的所有ConsumeQueue集合
        for (ConsumeQueue logic : maps.values()) {//遍歷同queueID下的所有ConsumeQueue
            logic.recover();//恢復消費隊列
            if (logic.getMaxPhysicOffset() > maxPhysicOffset) {
                maxPhysicOffset = logic.getMaxPhysicOffset();
            }
        }
    }

    return maxPhysicOffset;//返回所有消息隊列文件內消息在commitlog中的最大偏移量
}
//org.apache.rocketmq.store.ConsumeQueue.recover()
public void recover() {
    final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
    if (!mappedFiles.isEmpty()) {

        int index = mappedFiles.size() - 3;//最多恢復三個
        if (index < 0)
            index = 0;

        int mappedFileSizeLogics = this.mappedFileSize;//20字節
        MappedFile mappedFile = mappedFiles.get(index);
        ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();//共用同一個緩衝區,但是position各自獨立
        long processOffset = mappedFile.getFileFromOffset();//隊列文件名
        long mappedFileOffset = 0;
        long maxExtAddr = 1;
        while (true) {//消費隊列存儲單元是一個20字節定長數據,commitlog offset(8) + size(4) + message tag hashcode(8),commitlog offset是指這條消息在commitlog文件實際偏移量,size指消息大小,消息tag的哈希值,用於校驗
            for (int i = 0; i < mappedFileSizeLogics; i += CQ_STORE_UNIT_SIZE) {
                long offset = byteBuffer.getLong();//先讀取8字節,即commitlog offset
                int size = byteBuffer.getInt();//再讀取4字節,即msg size
                long tagsCode = byteBuffer.getLong();//再讀取8字節,即message tag hashcode

                if (offset >= 0 && size > 0) {
                    mappedFileOffset = i + CQ_STORE_UNIT_SIZE;//consumequeue上當前消息末尾位置,該值爲20*N,其中N是表示當前消息在consumequeue上是第幾個消息
                    this.maxPhysicOffset = offset;//隊列內消息在commitlog中的偏移量,this.maxPhysicOffset最終爲該隊列下的consumequeue文件內的消息在commitlog的最大物理偏移量,即在commitlog的位置,該值也就是commitlog轉儲到consumequeue的位置,該位置後的消息就需要轉儲到consumequeue
                    if (isExtAddr(tagsCode)) {//用於擴展的consumequeue,忽略,默認是不開啓,生產通常也不開啓,沒有研究過這個
                        maxExtAddr = tagsCode;
                    }
                } else {
                    log.info("recover current consume queue file over,  " + mappedFile.getFileName() + " "
                        + offset + " " + size + " " + tagsCode);
                    break;
                }
            }

            if (mappedFileOffset == mappedFileSizeLogics) {//達到consumequeue文件末尾
                index++;
                if (index >= mappedFiles.size()) {//遍歷到該隊列下是最後一個consumequeue文件則退出循環

                    log.info("recover last consume queue file over, last mapped file "
                        + mappedFile.getFileName());
                    break;
                } else {
                    mappedFile = mappedFiles.get(index);//獲取下一個mappedFile對象
                    byteBuffer = mappedFile.sliceByteBuffer();
                    processOffset = mappedFile.getFileFromOffset();//重置processOffset爲mappedFile文件名
                    mappedFileOffset = 0;
                    log.info("recover next consume queue file, " + mappedFile.getFileName());
                }
            } else {
                log.info("recover current consume queue queue over " + mappedFile.getFileName() + " "
                    + (processOffset + mappedFileOffset));
                break;
            }
        }

        processOffset += mappedFileOffset;//processOffset
        this.mappedFileQueue.setFlushedWhere(processOffset);//設置刷新位置
        this.mappedFileQueue.setCommittedWhere(processOffset);//設置提交位置
        this.mappedFileQueue.truncateDirtyFiles(processOffset);//清理大於指定offset的髒文件
        

        if (isExtReadEnable()) {//忽略,生產也不開啓擴展consumequeue
            this.consumeQueueExt.recover();
            log.info("Truncate consume queue extend file by max {}", maxExtAddr);
            this.consumeQueueExt.truncateByMaxAddress(maxExtAddr);
        }
    }
}

3.4.2.恢復commitlog

recoverConsumeQueue返回了已經刷盤到consumequeue的commitlog offset, 

3.4.2.1. 上次broker是正常關閉

org.apache.rocketmq.store.CommitLog.recoverNormally(long)大體邏輯和recoverConsumeQueue是相同的,不同之處是consumequeue每條記錄是固定20字節,而commitlog內每條記錄即一條消息,是變長的,commitlog格式如圖(該圖是網上截圖得來,具體格式可以在org.apache.rocketmq.store.CommitLog.DefaultAppendMessageCallback.doAppend(long, ByteBuffer, int, MessageExtBrokerInner)方法內查看)

recoverNormally操作也是最多恢復最新的三個commitlog文件,每次讀取commitlog中一條消息,最終獲取到commitlog對象已經刷盤的位置processOffset,並設置Commitlog對象的MappedFileQueue的刷盤flushedWhere和提交點committedWhere位置爲processOffset,清除髒commitlog文件。這個步驟邏輯級別和recoverConsumeQueue相同,不同之處是如果processOffset<=maxPhyOffsetOfConsumeQueue(該值是recoverConsumeQueue操作返回的commitlog已經轉儲到consumequeue的最大commitlog offset)的時候,需要執行org.apache.rocketmq.store.DefaultMessageStore.truncateDirtyLogicFiles(long)

//org.apache.rocketmq.store.DefaultMessageStore.truncateDirtyLogicFiles(long)
public void truncateDirtyLogicFiles(long phyOffset) {
    ConcurrentMap<String, ConcurrentMap<Integer, ConsumeQueue>> tables = DefaultMessageStore.this.consumeQueueTable;

    for (ConcurrentMap<Integer, ConsumeQueue> maps : tables.values()) {
        for (ConsumeQueue logic : maps.values()) {
            logic.truncateDirtyLogicFiles(phyOffset);
        }
    }
}
//org.apache.rocketmq.store.ConsumeQueue.truncateDirtyLogicFiles(long)
public void truncateDirtyLogicFiles(long phyOffet) {

    int logicFileSize = this.mappedFileSize;//600w

    this.maxPhysicOffset = phyOffet - 1;
    long maxExtAddr = 1;
    while (true) {
        MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();//獲取consumequeue的最後一個MappedFile,即文件名最大的那個MappedFile
        if (mappedFile != null) {
            ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();

            mappedFile.setWrotePosition(0);
            mappedFile.setCommittedPosition(0);
            mappedFile.setFlushedPosition(0);

            for (int i = 0; i < logicFileSize; i += CQ_STORE_UNIT_SIZE) {
                long offset = byteBuffer.getLong();//commitlog offset 8字節
                int size = byteBuffer.getInt();//msg size 4字節
                long tagsCode = byteBuffer.getLong();//tag hashcode 8字節

                if (0 == i) {//第一條消息
                    if (offset >= phyOffet) {
                        this.mappedFileQueue.deleteLastMappedFile();//從MappedFileQueue.mappedFiles移除最後的MappedFile,並釋放該MappedFile
                        break;//跳出for循環,繼續執行while循環,獲取刪除當前MappedFile後的mappedFileQueue的最後一個文件繼續重複執行該步驟
                    } else {
                        int pos = i + CQ_STORE_UNIT_SIZE;
                        mappedFile.setWrotePosition(pos);
                        mappedFile.setCommittedPosition(pos);
                        mappedFile.setFlushedPosition(pos);
                        this.maxPhysicOffset = offset;
                        // This maybe not take effect, when not every consume queue has extend file.
                        if (isExtAddr(tagsCode)) {//默認不使用擴展consumequeue.忽略
                            maxExtAddr = tagsCode;
                        }
                    }
                } else {

                    if (offset >= 0 && size > 0) {

                        if (offset >= phyOffet) {//MappedFile的非第一條消息offset>= phyOffet,說明當前的MappedFile既有
                            return;
                        }

                        int pos = i + CQ_STORE_UNIT_SIZE;
                        mappedFile.setWrotePosition(pos);
                        mappedFile.setCommittedPosition(pos);
                        mappedFile.setFlushedPosition(pos);
                        this.maxPhysicOffset = offset;
                        if (isExtAddr(tagsCode)) {//默認不使用擴展consumequeue.忽略
                            maxExtAddr = tagsCode;
                        }

                        if (pos == logicFileSize) {//達到當前MappedFile末尾,則結束
                            return;
                        }
                    } else {
                        return;
                    }
                }
            }
        } else {
            break;//mappedFile==null 退出while循環
        }
    }

    if (isExtReadEnable()) {//默認不使用擴展consumequeue.忽略
        this.consumeQueueExt.truncateByMaxAddress(maxExtAddr);
    }
}

DefaultMessageStore.truncateDirtyLogicFiles(long)方法,什麼情況下會進入?暫時沒搞清楚

3.4.2.2. 上次broker是異常關閉

org.apache.rocketmq.store.CommitLog.recoverAbnormally(long)跟recoverNormally正常關閉恢復commitlog基本相同,代碼加了註釋,看如下代碼

//org.apache.rocketmq.store.CommitLog.recoverAbnormally(long)
public void recoverAbnormally(long maxPhyOffsetOfConsumeQueue) {
  // recover by the minimum time stamp
  boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover();//true 默認校驗crc
  final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
  if (!mappedFiles.isEmpty()) {
      // Looking beginning to recover from which file
      int index = mappedFiles.size() - 1;
      MappedFile mappedFile = null;
      for (; index >= 0; index--) {//從commitlog文件倒序遍歷
          mappedFile = mappedFiles.get(index);
          if (this.isMappedFileMatchedRecover(mappedFile)) {//如果該commitlog的第一天消息存儲時間戳<=刷盤檢測點的刷盤時間,則說明該commitlog是需要恢復的,否則就繼續遍歷前一個commitlog
              log.info("recover from this mapped file " + mappedFile.getFileName());//recover from this mapped file
              break;
          }
      }

      //剩餘代碼邏輯等同recoverNormally
  }
  // Commitlog case files are deleted
  else {//如果不存在commitlog文件,比如過期被清除了or誤刪了
      this.mappedFileQueue.setFlushedWhere(0);//設置mappedFileQueue的刷盤位置爲0
      this.mappedFileQueue.setCommittedWhere(0);//設置mappedFileQueue的提交位置爲0
      this.defaultMessageStore.destroyLogics();//刪除所有的consumequeue文件
}

//org.apache.rocketmq.store.CommitLog.isMappedFileMatchedRecover(MappedFile)
/*
 * 	讀取該commitlog的第一條消息的存儲時間戳,如果時間戳<=Math.min(刷盤檢測點commitlog最後刷盤時間, 刷盤檢測點consumequeue最終刷盤時間 ) - 3s,
 * 	則說明該commitlog文件是需要進行恢復的
 */
private boolean isMappedFileMatchedRecover(final MappedFile mappedFile) {
    ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
		//commitlog組成 消息大小(4) +魔術(4)+消息報文體校驗碼(4)+隊列id(4)+flag(4)+當前消息在隊列中的第幾個(8)+物理地址偏移量(8)+SYSFLAG(4)+producer時間戳(8)+producer地址(8)+消息在broker存儲的時間(8)+消息存儲到broker的地址(8)+消息被重新消費了幾次(4)+prepard狀態的事務消息(8)+bodylenght(4)+body(消息體)+(1)+topicLength+(2)+propertiesLength
    int magicCode = byteBuffer.getInt(MessageDecoder.MESSAGE_MAGIC_CODE_POSTION);//獲取魔術daa320a7
    if (magicCode != MESSAGE_MAGIC_CODE) {//魔數不對,返回false,說明該commitlog不符合規則
        return false;
    }

    long storeTimestamp = byteBuffer.getLong(MessageDecoder.MESSAGE_STORE_TIMESTAMP_POSTION);//獲取存儲到broker的timestamp
    if (0 == storeTimestamp) {//存儲時間爲0,說明沒有消息,因爲commitlog創建後默認填充就是0x00
        return false;
    }

    if (this.defaultMessageStore.getMessageStoreConfig().isMessageIndexEnable()
        && this.defaultMessageStore.getMessageStoreConfig().isMessageIndexSafe()) {//默認false
        if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestampIndex()) {
            log.info("find check timestamp, {} {}",
                storeTimestamp,
                UtilAll.timeMillisToHumanString(storeTimestamp));
            return true;
        }
    } else {//執行這裏
        if (storeTimestamp <= this.defaultMessageStore.getStoreCheckpoint().getMinTimestamp()) {//該commitlog第一條消息存儲時間戳<=Math.min(刷盤檢測點commitlog最後刷盤時間, 刷盤檢測點consumequeue最終刷盤時間 ) - 3s
            log.info("find check timestamp, {} {}",
                storeTimestamp,
                UtilAll.timeMillisToHumanString(storeTimestamp));
            return true;
        }
    }

    return false;
}

4.broker運行中消息寫入commitlog保存

producer發送消息到broker,發送命令是SEND_MESSAGE_V2,broker端對應的處理器是SendMessageProcessor,如下圖,是broker端收到消息後的處理堆棧(紅框內)

核心方法是org.apache.rocketmq.store.CommitLog.putMessage(MessageExtBrokerInner),代碼如下,註釋加的比較清楚,應該對於邏輯比較容易懂

public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
        // Set the storage time
        msg.setStoreTimestamp(System.currentTimeMillis());//設置消息落地時間
        // Set the message body BODY CRC (consider the most appropriate setting
        // on the client)
        msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
        // Back to Results
        AppendMessageResult result = null;

        StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();//DefaultMessageStore.storeStatsService

        String topic = msg.getTopic();
        int queueId = msg.getQueueId();

        final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());//從消息的SYSFlag判斷消息類型
        if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
            || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {//事務消息,非事務消息
            // Delay Delivery
            if (msg.getDelayTimeLevel() > 0) {//如果消息的延遲級別>0 說明消息設置了延時
            	/*
            	 * 	如果消息的延遲級別>0 將消息的原topicName和原消息隊列ID存入消息屬性中,
            	 * 	用延遲消息主題SCHEDULE_TOPIC_XXXX,消息隊列ID更新原消息的主題與隊列
            	 * 	對於producer發送的延時消息,消費端消費失敗重發的消息都是有延時級別,都會被更改topic保存到commitlog
            	 */
                if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
                    msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
                }
                //	對於重發交易,它的延時級別都是大於0的,因此重發交易在消費失敗的時候發送到broker保存的時候都先被更高爲重試topic,接着在保存到commitlog的時候被保存爲schedule主題
                topic = ScheduleMessageService.SCHEDULE_TOPIC;
                queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());

                // Backup real topic, queueId
                MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());//將消息的原topicName和原消息隊列ID存入消息屬性中,
                MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
                msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));

                msg.setTopic(topic);
                msg.setQueueId(queueId);
            }
        }
        //Commit消息 包括普通消息, 重發消息,延時消息,事務消息
        long eclipseTimeInLock = 0;
        MappedFile unlockMappedFile = null;
        MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();//獲取當前可以寫入的commitlog文件,獲取一個MappedFile對象,內存映射的具體實現

        //消息寫入是同步操作
        putMessageLock.lock(); //spin or ReentrantLock ,depending on store config 加鎖 默認使用自旋鎖加鎖 即cas加鎖
        try {
            long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();//加鎖時間 當前時間
            this.beginTimeInLock = beginLockTimestamp;

            // Here settings are stored timestamp, in order to ensure an orderly
            // global
            msg.setStoreTimestamp(beginLockTimestamp);//設置消息存儲時間戳

            if (null == mappedFile || mappedFile.isFull()) {
                /*
                 * 	如果mappedFile==null 則需要新建mappedFile對象,即新建commitlog
                 * 	如果mappedFile.isFull() 意思是commitlog文件滿了,mappedFile.wrotePosition==1024^3,寫道了文件末尾
                 *	 創建commitlog文件,創建mappedFile對象
                 */
            	mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
            }
            if (null == mappedFile) {
                log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
            }

            result = mappedFile.appendMessage(msg, this.appendMessageCallback);//消息寫入commitlog文件對應的pagecache
            switch (result.getStatus()) {
                case PUT_OK:
                    break;//寫入成功
                case END_OF_FILE://文commitlog剩餘空間不足寫入該消息,則新建commitlog文件,通常創建commitlog是走該分支,mappedFile.isFull()的情況太罕見了,需要恰好一條消息的長度剩餘commitlog的剩餘寫入空間
                    unlockMappedFile = mappedFile;
                    // Create a new file, re-write the message
                    mappedFile = this.mappedFileQueue.getLastMappedFile(0);//新建commitlog文件
                    if (null == mappedFile) {
                        // XXX: warn and notify me
                        log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                        beginTimeInLock = 0;
                        return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
                    }
                    result = mappedFile.appendMessage(msg, this.appendMessageCallback);//消息寫入新commitlog文件對應的pagecache
                    break;
                case MESSAGE_SIZE_EXCEEDED:
                case PROPERTIES_SIZE_EXCEEDED:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
                case UNKNOWN_ERROR:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
                default:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
            }

            eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
            beginTimeInLock = 0;
        } finally {
            putMessageLock.unlock();//cas解鎖
        }

        if (eclipseTimeInLock > 500) {//消息寫入pagecache耗時超過500ms報警
            log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, msg.getBody().length, result);
        }

        //如果broker開啓了文件預熱warmMapedFileEnable=true,則通過jna釋放commitlog創建時候pagecache鎖定的物理內存。生產通常是開啓文件預熱的,避免日誌文件在分配內存時缺頁中斷
        if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
            this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
        }

        PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);//設置返回producer的結果

        // Statistics
        storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
        storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());

        handleDiskFlush(result, putMessageResult, msg);//刷盤,吧消息保存到磁盤
        handleHA(result, putMessageResult, msg);//ha同步消息

        return putMessageResult;
    }

4.1.消息寫入到commitlog

消息寫入到commitlog,即追加到pagecache的方法,如下方法

//org.apache.rocketmq.store.MappedFile.appendMessagesInner(MessageExt, AppendMessageCallback)
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {//cb=DefaultAppendMessageCallback
    assert messageExt != null;
    assert cb != null;

    int currentPos = this.wrotePosition.get();//當前MappedFile的寫位置

    if (currentPos < this.fileSize) {//當前MappedFile的寫位置<文件大小,說明文件有剩餘位置可寫
    	/*
    	 * 	僅當transientStorePoolEnable 爲true,刷盤策略爲異步刷盤(FlushDiskType爲ASYNC_FLUSH),
    	 * 	並且broker爲主節點時,才啓用堆外分配內存。此時:writeBuffer不爲null
    	 * 	Buffer與同步和異步刷盤相關
    	 * 	writeBuffer/mappedByteBuffer的position始終爲0,而limit則始終等於capacity
    	 * 	slice創建一個新的buffer, 是根據position和limit來生成byteBuffer,與原buf共享同一內存
    	 * 	開啓了transientStorePoolEnable,數據是寫入到堆外內存,即this.writeBuffer
    	 */
        ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();//重點
        byteBuffer.position(currentPos);//設置寫位置
        AppendMessageResult result = null;
        if (messageExt instanceof MessageExtBrokerInner) {//根據消息類型,是批量消息還是單個消息,進入相應的處理
        	//處理單個消息, this.fileSize - currentPos是可寫的空間
        	result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
        } else if (messageExt instanceof MessageExtBatch) {
        	 //處理批量消息
        	result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
        } else {
            return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
        }
        this.wrotePosition.addAndGet(result.getWroteBytes());//把MappedFile中的寫位置更新爲寫了消息之後的位置
        this.storeTimestamp = result.getStoreTimestamp();//更新storeTimestamp爲消息存儲時間戳,即broker接收到消息的時間戳
        return result;
    }
    //寫滿會報錯,正常不會進入該代碼,調用該方法前有判斷
    log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
    return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}

以追加單個消息爲例說明org.apache.rocketmq.store.CommitLog.DefaultAppendMessageCallback.doAppend(long, ByteBuffer, int, MessageExtBrokerInner),該方法就是按照commitlog格式吧消息寫入到傳入的參數ByteBuffer內,如果是開啓了transientStorePoolEnable=true,則該ByteBuffer是MappedFile.writeBuffer堆外內存,未開啓則是MappedFile.mappedByteBuffer即pagecache。該pagecache是對應具體的commitlog磁盤文件,該writeBuffer是對應什麼呢?在刷盤服務線程下會把writeBuffer內的數據寫入到pagecache,再由pagecache最終刷新保存到commitlog磁盤上。下面到刷盤時候自然清楚。因此開啓transientStorePoolEnable=true的情況下是吧消息寫入堆外內存即MappedFile.writeBuffer,未開啓情況下是寫入到pagecache即MappedFile.mappedByteBuffer。

說到這裏,在commitlog剩餘空間不足以寫入該消息,需要新建commitlog,那麼是如何實現的?

在org.apache.rocketmq.store.CommitLog.putMessage(MessageExtBrokerInner)內的

mappedFile = this.mappedFileQueue.getLastMappedFile(0)

這行代碼實現的

//org.apache.rocketmq.store.MappedFileQueue.getLastMappedFile(long, boolean)
/*
 *	 功能:獲取最新的MappedFile對象
 *	如果沒有或者上個commitlog/consumequeue已經滿了,則新創建一個commitlog/consumequeue文件
 *	對於創建commitlog來說,通常創建的原因是剩餘空間不足一寫入一條消息,傳入參數startOffset==0 , needCreate=true
 *	對於創建consumequeue來說,通常創建的原因是因爲真滿了,因爲consumequeue是固定長度,傳入參數startOffset==300w*20 , needCreate=true
 */
public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
    long createOffset = -1;
    MappedFile mappedFileLast = getLastMappedFile();//獲取最新的MappedFile

    // 	一個映射文件都不存在 createOffset=0
    if (mappedFileLast == null) { 
        createOffset = startOffset - (startOffset % this.mappedFileSize);
    }

    //	已經存在了MappedFile文件,且文件滿了則創建
    if (mappedFileLast != null && mappedFileLast.isFull()) {//mappedFileLast.isFull()對於commitlog來說是很罕見遇到的,因此創建commitlog通常不走這裏,但是由於consumequeue每個消息是固定長度,每次是會寫滿通常走這裏
        createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;//createOffset是上個文件名+文件size,用作新建的MappedFile文件名。公式就是createOffset=(N-1)*this.mappedFileSize 其中N爲第幾個MappedFile
    }

    //	創建新的commitlog/consumequeue文件對象MappedFile
    if (createOffset != -1 && needCreate) {//對於commitlog創建來說,通常執行到這裏createOffset==0
        String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);//以createOffset作爲commitlog or consumequeue的文件名
        String nextNextFilePath = this.storePath + File.separator
            + UtilAll.offset2FileName(createOffset + this.mappedFileSize);
        MappedFile mappedFile = null;

        //	創建commitlog走這裏,因爲開啓後this.allocateMappedFileService爲AllocateMappedFileService,在broker啓動時候new Commitlog的時候賦值的
        if (this.allocateMappedFileService != null) {//true
            mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
                nextNextFilePath, this.mappedFileSize);//由AllocateMappedFileService線程異步創建mappedFile,並同步等待獲取創建結果
        } else {//consumequeue文件創建走這裏
            try {
                mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);//創建consumequeue對應的MappedFile
            } catch (IOException e) {
                log.error("create mappedFile exception", e);
            }
        }

        if (mappedFile != null) {
            if (this.mappedFiles.isEmpty()) {
                mappedFile.setFirstCreateInQueue(true);//第一個設置MappedFile.firstCreateInQueue=true
            }
            this.mappedFiles.add(mappedFile);//添加到MappedFileQueue集合
        }

        return mappedFile;//文件滿了則返回新建的文件
    }

    return mappedFileLast;//文件未滿則返回最新的MappedFile
}

這裏有個難點是mappedFileLast.isFull()的判斷,因爲對於consumequeue來說每條記錄是固定20字節,寫位置是每次達到末尾(即文件size)結束。但是對於commitlog來說,每條記錄(消息)是變長的,比如要寫入的消息長度是200字節,但是commitlog只有70字節空間,因此這樣情況是需要新建commitlog文件,吧消息寫入到新的commitlog內,但是舊的commitlog的寫位置MappedFile.wrotePosition怎麼就變成了MappedFile.fileSize呢?因爲MappedFile.wrotePosition變成MappedFile.fileSize是很恰好發生的事情,加入待寫入的消息長度和commitlog剩餘空間恰好相等,則說明正好容納,這種很罕見的情況,在寫完後wrotePosition==fileSize,但是實際也不會發生,因爲如果寫入,MappedFile.wrotePosition最大隻能達到MappedFile.fileSize-8,因爲commitlog預留末尾8字節作爲commitlog結束。那麼是如何使剩餘空間不足夠寫入消息而讓wrotePosition==fileSize呢?經過分析得知在寫入消息方法org.apache.rocketmq.store.MappedFile.appendMessagesInner(MessageExt, AppendMessageCallback)內,追加消息到commitlog後,執行this.wrotePosition.addAndGet(result.getWroteBytes());,這裏result.getWroteBytes()是AppendMessageResult.wroteBytes,該值是在AppendMessageResult構造器賦值,在org.apache.rocketmq.store.CommitLog.DefaultAppendMessageCallback.doAppend(long, ByteBuffer, int, MessageExtBrokerInner)內END_OF_FILE這行,如果消息長度+8字節(commitlog文件結束的保留字節)>commitlog剩餘空間maxBlank,則構造AppendMessageResult對象,AppendMessageResult.wroteBytes=maxBlank,而maxBlank==MappedFile.fileSize-MappedFile.wrotePosition,那麼在commitlog剩餘空間不足寫入該條消息的情況下,在返回AppendMessageResult對象END_OF_FILE錯誤後,寫位置變爲MappedFile.wrotePosition+maxBlank=MappedFile.wrotePosition+MappedFile.fileSize-MappedFile.wrotePosition=MappedFile.fileSize,即在剩餘空間不足寫入該條消息的時候,寫位置會被更新爲文件大小,表示文件滿了,需要創建文件。

4.2.消息刷盤

消息寫入到pagechace/堆外內存後,執行刷盤handleDiskFlush(消息保存到磁盤),分爲同步刷盤和異步刷盤,入口方法如下

//org.apache.rocketmq.store.CommitLog.handleDiskFlush(AppendMessageResult, PutMessageResult, MessageExt)
public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
    // Synchronization flush
	//同步刷寫,這裏有兩種配置,是否一定要收到存儲MSG信息,才返回,默認爲true
    if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
        final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
        if (messageExt.isWaitStoreMsgOK()) {//默認是true, 代碼@1
            GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());//代碼@2 result.getWroteOffset() + result.getWroteBytes()即爲MappedFile寫入消息後的MappedFile.wrotePosition+MappedFile.fileFromOffset
            service.putRequest(request);//代碼@3
            boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());////代碼@4
            if (!flushOK) {//即GroupCommitRequest.flushOK爲false,表示刷盤超時,那麼設置返回結果爲超時
                log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
                    + " client address: " + messageExt.getBornHostString());
                putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
            }
        } else {//代碼@5 通常不走該分支,既然是同步刷盤,那麼爲了保證100%的消息不丟失,只能是消息寫入到磁盤後纔不會丟失,那麼就只能同步等待消息寫入到磁盤後才能返回。
            service.wakeup();//喚醒同步刷盤線程進行刷屏。
        }
    }
    // Asynchronous flush
    else {
        if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {//未開啓transientStorePoolEnable=true且異步刷盤
            flushCommitLogService.wakeup();//代碼@6  異步刷盤
        } else {//開啓了transientStorePoolEnable=true且異步刷盤
            commitLogService.wakeup();//代碼@7 喚醒把消息寫入pagecache的線程
        }
    }
}

這裏解釋下爲什麼代碼@1處默認是true,因爲通常producer創建消息採用的構造器是

//producer構造Message通常使用下面三個構造器
Message(String topic, byte[] body)
Message(String topic, String tags, byte[] body)
Message(String topic, String tags, String keys, byte[] body)
//對於這三個構造器,均設置 this.setWaitStoreMsgOK(true),即屬性PROPERTY_WAIT_STORE_MSG_OK均設置爲true
//基本不使用的構造器
public Message(String topic, String tags, String keys, int flag, byte[] body, boolean waitStoreMsgOK)
//該構造器可以指定PROPERTY_WAIT_STORE_MSG_OK屬性爲false

因此默認通常PROPERTY_WAIT_STORE_MSG_OK是爲true的,表示的含義是等待收到消息存儲成功。可否使用第四個構造器設置waitStoreMsgOK=false呢?也是可以的,但是這樣的會在handleDiskFlush內同步刷盤就走到了代碼@5分支,這就無法保證同步刷盤的100%消息落地,而且這樣的效率不如異步刷盤,爲什麼不使用異步刷盤呢,因此代碼@5分支實際沒人使用。

this.flushCommitLogServic是個刷盤服務類FlushCommitLogService的具體子類,是個runnable對象,根據不同的刷盤方式賦值爲不同的對象,同步刷盤爲GroupCommitService,異步刷盤爲FlushRealTimeService,在broker啓動中org.apache.rocketmq.store.CommitLog.start()啓動該服務線程的,調用關係如下

4.2.1.同步刷盤

在handleDiskFlush內代碼解釋

代碼@2,result.getWroteOffset() + result.getWroteBytes()即爲MappedFile寫入消息後的MappedFile.wrotePosition+MappedFile.fileFromOffset,即爲整個commitlog對象(也爲MappedFileQueue,因爲代表MappedFile集合)上的位置,MappedFileQueue.flushedWhere到該位置之間的數據就是需要刷新到磁盤的。

代碼@3,吧GroupCommitRequest保存到GroupCommitService.requestsWrite集合,並喚醒阻塞的GroupCommitService服務線程(同步刷盤線程)

public synchronized void putRequest(final GroupCommitRequest request) {//handleDiskFlush()操作執行
            synchronized (this.requestsWrite) {
                this.requestsWrite.add(request);
            }
            if (hasNotified.compareAndSet(false, true)) {
                waitPoint.countDown(); // notify 喚醒阻塞的GroupCommitService服務線程
            }
        }

代碼@4,就是同步等待數據落盤,阻塞操作。

具體同步刷盤方式是GroupCommitService線程不停的輪詢,並提交

//org.apache.rocketmq.store.CommitLog.GroupCommitService.run()
public void run() {
    CommitLog.log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            this.waitForRunning(10);
            this.doCommit();
        } catch (Exception e) {
            CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

    //省略其他代碼
}

接着看GroupCommitService.waitForRunning(long)方法

protected void waitForRunning(long interval) {
	if (hasNotified.compareAndSet(true, false)) {
	    this.onWaitEnd();
	    return;
	}
	//entry to wait
	waitPoint.reset();
	
	try {
	    waitPoint.await(interval, TimeUnit.MILLISECONDS);
	} catch (InterruptedException e) {
	    log.error("Interrupted", e);
	} finally {
	    hasNotified.set(false);
	    this.onWaitEnd();
	}
}
@Override
protected void onWaitEnd() {
    this.swapRequests();
}
private void swapRequests() {//交換兩個集合數據,這樣在handleDiskFlush添加到requestsWrite的GroupCommitRequest對象就被交換到了requestsRead
    List<GroupCommitRequest> tmp = this.requestsWrite;
    this.requestsWrite = this.requestsRead;
    this.requestsRead = tmp;
}

在同步刷盤服務線程不停輪詢的過程中,每次把handleDiskFlush內添加到requestsWrite的GroupCommitRequest對象交換到了requestsRead,這個設計不錯,進行了讀寫分離,但是爲什麼要設計的這麼麻煩呢?直接在handleDiskFlush內把GroupCommitRequest對象提交到GroupCommitService的一個阻塞隊列不行麼?難道是效率什麼的考慮,

接着執行刷盤操作doCommit,代碼如下,註釋很清楚了,就不寫說明了。

private void doCommit() {
    synchronized (this.requestsRead) {
        if (!this.requestsRead.isEmpty()) {
            for (GroupCommitRequest req : this.requestsRead) {//this.requestsRead雖然是集合,但是實際有且只會有一個元素,定爲集合是爲了考慮將來擴展吧
                // There may be a message in the next file, so a maximum of
                // two times the flush
                boolean flushOK = false;
                for (int i = 0; i < 2 && !flushOK; i++) {//循環第一次,flushOK爲false,那麼進行刷盤,接着循環第二次的時候,flushOK被置爲true,退出for循環
                	//如果commitlog上次刷盤點MappedFileQueue.flushedWhere>=本次待刷盤位置,則不進行刷盤,接着退出for循環
                    flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();//for循環第一次爲false,第二次爲true

                    if (!flushOK) {
                        CommitLog.this.mappedFileQueue.flush(0);//執行刷盤操作,把消息從pagecache寫入到磁盤保存
                    }
                }

                req.wakeupCustomer(flushOK);//設置GroupCommitRequest.flushOK爲true,表示刷盤成功,該值在handleDiskFlush內用作判斷在等待時間內刷盤是否成功。喚醒用戶線程,即喚醒handleDiskFlush的代碼@3
            }

            long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
            if (storeTimestamp > 0) {
            	//每次刷盤後把刷盤時間戳保存到StoreCheckpoint.physicMsgTimestamp,以供broker異常關閉後啓動恢復刷盤位置,這裏和broker啓動進行recover操作對應
                CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
            }

            this.requestsRead.clear();//清空
        } else {
            // Because of individual messages is set to not sync flush, it
            // will come to this process
            CommitLog.this.mappedFileQueue.flush(0);//這裏對應着handleDiskFlush的代碼@5,直接就刷盤
        }
    }
}

 

4.2.2.異步刷盤

4.2.2.1.異步刷盤未開啓transientStorePoolEnable=true的情況

handleDiskFlush的代碼@6 處是未開啓transientStorePoolEnable=true且異步刷盤的情況,執行FlushRealTimeService.wakeup(),喚醒異步刷盤服務線程FlushRealTimeService

異步刷盤服務線程的執行

//org.apache.rocketmq.store.CommitLog.FlushRealTimeService.run()
public void run() {
    CommitLog.log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();//false

        int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();//異步刷盤間隔時間500ms
        int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();//默認操作系統提交頁數 4
		
		//省略不重要代碼
       
        try {
            //省略不重要代碼
            long begin = System.currentTimeMillis();
            CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);//把數據刷新到磁盤
            long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
            if (storeTimestamp > 0) {
            	//每次刷盤後把刷盤時間戳保存到StoreCheckpoint.physicMsgTimestamp,以供broker異常關閉後啓動恢復刷盤位置,這裏和broker啓動進行recover操作對應
                CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
            }
            long past = System.currentTimeMillis() - begin;
            if (past > 500) {
                log.info("Flush data to disk costs {} ms", past);
            }
        } catch (Throwable e) {
            CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
            this.printFlushProgress();
        }
    }//while end

    // Normal shutdown, to ensure that all the flush before exit
    boolean result = false;
    //如果broker被關閉,則執行到這裏。由於是異步刷盤,此時磁盤數據落後commitlog,那麼盡力吧commitlog數據刷新到磁盤
    for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
        result = CommitLog.this.mappedFileQueue.flush(0);
        CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
    }

    //省略不重要代碼

    CommitLog.log.info(this.getServiceName() + " service end");
}

異步刷盤較同步刷盤邏輯簡單,僅僅是刷盤而已,沒有那麼多的控制。

4.2.2.2.異步刷盤開啓transientStorePoolEnable=true的情況

handleDiskFlush的代碼@7 處是開啓transientStorePoolEnable=true且異步刷盤的情況,執行CommitRealTimeService.wakeup(),喚醒異步刷盤服務線程CommitRealTimeService,這個和FlushRealTimeService不同

具體執行看代碼和其中註釋

//org.apache.rocketmq.store.CommitLog.CommitRealTimeService.run()
public void run() {
    CommitLog.log.info(this.getServiceName() + " service started");
    while (!this.isStopped()) {
        int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();//刷盤頻率 200ms

        int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();//默認操作系統頁 4

        int commitDataThoroughInterval =
            CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();//最大提交數據間隔時間 200ms,設置該值可以提高broker性能

        long begin = System.currentTimeMillis();
        if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
            this.lastCommitTimestamp = begin;
            commitDataLeastPages = 0;//如果上次提交數據的時間戳+commitDataThoroughInterval<=當前時間,說明消息過少,不滿足4頁的提交,在達到最大提交時間後,強制不滿足4頁提交條件也提交數據到pagecache
        }

        try {
            boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);//把數據由堆外內存MappedFile.writeBuffer提交到pagecache即MappedFile.mappedByteBuffer保存
            long end = System.currentTimeMillis();
            if (!result) {//result爲true說明本次沒有數據提交,具體產生該情況待提交的數據不滿足4k,因此實際是沒有提交。
                this.lastCommitTimestamp = end; // result = false means some data committed.
                //now wake up flush thread.
                flushCommitLogService.wakeup();//喚醒異步刷盤線程FlushRealTimeService,異步刷盤線程把消息從pagecache保存到磁盤
            }

            if (end - begin > 500) {
                log.info("Commit data to file costs {} ms", end - begin);
            }
            this.waitForRunning(interval);
        } catch (Throwable e) {
            CommitLog.log.error(this.getServiceName() + " service has exception. ", e);
        }
    }//end while

    //如果broker被關閉,則執行到這裏。由於是異步刷盤,此時磁盤數據落後commitlog,那麼盡力吧commitlog數據提交到pagecache保存,這樣儘管broker關閉了,但是數據是保存在操作系統,因此是不會丟失的
    boolean result = false;
    for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
        result = CommitLog.this.mappedFileQueue.commit(0);//把待刷盤的數據提交到pagecache保存
        CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
    }
    CommitLog.log.info(this.getServiceName() + " service end");
}

 總的邏輯就是先把堆外內存數據提交到pagecache,然後喚醒異步刷盤服務線程進行刷盤,由異步刷盤線程吧pagecache內的數據最終刷新到磁盤上保存。

4.2.3三種刷盤方式比較總結

case1:同步刷盤GroupCommitService,producer發送消息到broker,broker保存消息到pagecache,然後同步等待數據落盤後,把結果返回producer,這樣producer就知道具體消息是否發送成功。

異步刷盤FlushRealTimeService分兩種情況

case2:未開啓transientStorePoolEnable=true情況

producer發送消息到broker,broker保存消息到pagecache即MappedFile.mappedByteBuffer,喚醒異步刷盤線程,然後把結果返回producer,這樣producer收到返回結果發送成功,但是不一定就消息就真的保存到了磁盤,這樣有小概率會丟失消息。

broker異步刷盤線程FlushRealTimeService把pagecache的消息通過固定頻率刷新保存到磁盤。

case3:開啓transientStorePoolEnable=true情況

producer發送消息到broker,broker保存消息到堆外內存即MappedFile.writeBuffer,喚醒異步提交線程,然後把結果返回producer,這樣producer收到返回結果發送成功,但是不一定就消息就真的保存到了磁盤,這樣有小概率會丟失消息。

broker異步提交線程CommitRealTimeService把堆外內存的消息通過固定頻率提交到pagecache,喚醒異步刷盤線程FlushRealTimeService,由該線程把pagecache消息刷新保存到磁盤。

總結:對於case1,不會丟失發送的消息,但是效率不高,因爲把同步等待吧消息寫入到磁盤速度是較慢的(rmq採用的是順序寫,速度也不慢)。對於case2、case3,發送消息效率高,但是如果服務器宕機,那麼會丟失少量未提交保存的消息。對於case2,如果只是broker掛了,但是消息是保存在pagecache的,那麼也不會丟失消息。對於case3,由於消息是寫入到堆外內存的,如果broker掛了,自然就沒有指向該堆外內存的指針了,因此堆外內存未提交到pagecache的少量消息會丟失,但是case3效率也是最高的,不僅發送消息效率高,而且消費效率也高,因爲對於case3情況,是讀寫分離,消息發送是寫入堆外內存,消息消費是從pagecache讀取,消息的消費後續會再寫博客。

4.2.4.刷盤的核心實現

從4.2.2可以看出,不論是異步刷盤or同步刷盤,最終都是執行org.apache.rocketmq.store.MappedFileQueue.flush(int)

具體代碼加了註釋,註釋已經把完整功能都描述了,請看代碼

/*
 * 在刷盤服務線程FlushRealTimeService、GroupCommitService內刷盤調用
 * 功能就是把pagecache內的數據刷新保存到磁盤
 * 參數flushLeastPages在同步刷盤爲0,異步刷盤爲4
 * 結果返回true說明本次刷盤並沒有真正執行(沒有數據刷新到磁盤),比如異步刷盤數據不滿足4k的要求,結果爲false說明有數據被刷新到了磁盤
 */
 //org.apache.rocketmq.store.MappedFileQueue.flush(int)
public boolean flush(final int flushLeastPages) {
    boolean result = true;
    MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, this.flushedWhere == 0);//根據上次刷新的位置,得到當前的MappedFile對象
    if (mappedFile != null) {
        long tmpTimeStamp = mappedFile.getStoreTimestamp();//獲取mappedFile最後一天消息的存儲時間戳
        int offset = mappedFile.flush(flushLeastPages);//把數據保存到磁盤文件 並返回最新刷盤位置
        long where = mappedFile.getFileFromOffset() + offset;//相對於MappedFileQueue的刷盤位置
        result = where == this.flushedWhere;//如果有待刷新的數據被刷新到了磁盤,則返回false。如果此時broker很空閒,沒有待寫入的數據(即MappedFile.wrotePosition==MappedFile.flushedPosition),那麼就返回true。還要就是異步刷盤不滿足4k的刷盤要求,實際也並不進行刷盤,也返回true.
        this.flushedWhere = where;//更新刷新位置爲新刷新位置
        if (0 == flushLeastPages) {
            this.storeTimestamp = tmpTimeStamp;
        }
    }

    return result;
}
/*
 * 參數flushLeastPages在同步刷盤爲0,異步刷盤爲4
 * 功能就是把pagecache內的數據刷新保存到磁盤
 * 同步刷盤就是把flushedPosition->wrotePosition之間的數據刷新到磁盤,只要這倆個位置不同,就會每次執行都把數據刷新到磁盤
 * 異步刷盤(未開啓transientStorePoolEnable=true)就是把flushedPosition->wrotePosition之間的數據刷新到磁盤,只有這倆個位置差距大於4k情況,纔會把數據刷新到磁盤,實際並不會每次執行flush操作都會把數據刷新到磁盤的
 * 異步刷盤(開啓transientStorePoolEnable=true)就是把committedPosition->wrotePosition之間的數據刷新到磁盤,只有這倆個位置差距大於4k情況,纔會把數據刷新到磁盤,實際並不會每次執行flush操作都會把數據刷新到磁盤的
 */
 //org.apache.rocketmq.store.MappedFile.flush(int)
public int flush(final int flushLeastPages) {
    if (this.isAbleToFlush(flushLeastPages)) {//如果MappedFile被寫滿,則需要刷新。MappedFile.wrotePosition(或者committedPosition)>MappedFile.flushedPosition,則說明可以寫(同步刷盤每次執行都刷新(只要有數據待刷新)),但是對於異步刷盤來說,只有滿足待刷新數據大於4k纔會刷盤,這樣是爲了性能考慮
        if (this.hold()) {
            int value = getReadPosition();//readPosition,同步刷盤和異步刷盤未開啓transientStorePoolEnable=true的情況返回MappedFile.wrotePosition,異步刷盤且開啓transientStorePoolEnable=true的情況返回MappedFile.committedPosition

            try {
                //We only append data to fileChannel or mappedByteBuffer, never both.
                if (writeBuffer != null || this.fileChannel.position() != 0) {//異步刷盤且開啓了transientStorePoolEnable=true執行這裏,把文件通道內的數據寫入到磁盤保存
                    this.fileChannel.force(false);//如果開啓了transientStorePoolEnable=true,則把堆外內存內的數據刷入到磁盤。這裏採用的不是pagecache,那麼在master宕機的時候,會損失消息
                } else {//同步刷盤和異步刷盤未開啓transientStorePoolEnable=true的情況走這裏
                    this.mappedByteBuffer.force();//把pagecache內的數據刷入到磁盤
                }
            } catch (Throwable e) {
                log.error("Error occurred when force data to disk.", e);
            }

            this.flushedPosition.set(value);//更新刷盤位置flushedPosition爲readPosition,對於同步刷盤和異步刷盤未開啓transientStorePoolEnable=true的情況爲wrotePosition or 對於異步刷盤且開啓transientStorePoolEnable=true的情況爲committedPosition
            this.release();//每次刷盤動作實際不會釋放pagecahe的物理內存,因爲只有在MappedFile被銷燬的時候才MappedFile.available纔會被更新爲false,只有在被更新爲false情況下才會被釋放
        } else {
            log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
            this.flushedPosition.set(getReadPosition());
        }
    }
    return this.getFlushedPosition();//返回刷盤位置MappedFile.flushedPosition
}

數據提交,即把堆外內存的數據提交到pagecache內,直接看代碼和註釋,功能和flush差不多

/*
 * 傳入參數commitLeastPages爲0 or 4,爲0表示需要強制提交,爲4表示按照4k頁提交。有強制提交是因爲消息過少,那麼滿足4k的提交情況時間跨度比較大,而且關閉broker的時候(需要提交)消息不滿足4k就不能提交了,因此有了強制提交
 * 如果沒有開啓transientStorePoolEnable=true則不會執行這裏。具體是在CommitReadTimeService服務線程執行
 * 功能:把數據由堆外內存提交到pagecache,僅限於commitlog文件,並返回提交結果,爲true表示本次並沒有真正提交,爲false表示本次真正提交了數據到pagecache
 */
 //org.apache.rocketmq.store.MappedFileQueue.commit(int)
public boolean commit(final int commitLeastPages) {
    boolean result = true;
    MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, this.committedWhere == 0);//根據上次提交位置,得到對應的MappedFile對象
    if (mappedFile != null) {
        int offset = mappedFile.commit(commitLeastPages);
        long where = mappedFile.getFileFromOffset() + offset;//新提交點,可能跟原提交點相同,因爲在commitLeastPages==4的情況下,不滿足4k情況是不提交的
        result = where == this.committedWhere;
        this.committedWhere = where;//更新MappedFileQueue.committedWhere
    }

    return result;
}
/*
 * 傳入參數commitLeastPages爲0 or 4,爲0表示需要強制提交,爲4表示按照4k頁提交
 * 把數據由堆外內存寫入到pagecache內,然後返回新提交位置(即寫的位置)
 */
 //org.apache.rocketmq.store.MappedFile.commit(int)
public int commit(final int commitLeastPages) {
    if (writeBuffer == null) {//沒有開啓transientStorePoolEnable=true的時候,不把消息寫入pagecache,返回寫位置。即沒有開啓transientStorePoolEnable的情況下committedPosition就沒有意義
        //no need to commit data to file channel, so just regard wrotePosition as committedPosition.
        return this.wrotePosition.get();
    }
    if (this.isAbleToCommit(commitLeastPages)) {//如果MappedFile被寫滿,則需要刷新。或者 MappedFile.wrotePosition與MappedFile.committedPosition滿足待提交數據大於4k纔會提交,這樣是爲了性能考慮
        if (this.hold()) {
            commit0(commitLeastPages);//把堆外內存committedPosition->wrotePosition之間的數據提交到pagecache,並更新committedPosition爲wrotePosition
            this.release();//每次提交動作實際不會釋放pagecache的物理內存,因爲只有在MappedFile被銷燬的時候才MappedFile.available纔會被更新爲false,只有在被更新爲false情況下才會被釋放
        } else {
            log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
        }
    }

    // All dirty data has been committed to FileChannel.
    if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {//如果提交位置committedPosition達到了文件末尾,則該文件數據已經全部被提交到了pagecache,那麼堆外內存就可以釋放了
        this.transientStorePool.returnBuffer(writeBuffer);//歸還使用的堆外內存到緩衝池
        this.writeBuffer = null;
    }

    return this.committedPosition.get();//返回最新的committedPosition
}
/*
 * 把this.wrotePosition - this.committedPosition之間的數據寫入到pagecache,同時把this.committedPosition更新爲this.wrotePosition
 */
 //org.apache.rocketmq.store.MappedFile.commit0(int)
protected void commit0(final int commitLeastPages) {
    int writePos = this.wrotePosition.get();
    int lastCommittedPosition = this.committedPosition.get();

    if (writePos - this.committedPosition.get() > 0) {
        try {
            ByteBuffer byteBuffer = writeBuffer.slice();
            byteBuffer.position(lastCommittedPosition);
            byteBuffer.limit(writePos);//堆外內存committedPosition->wrotePosition之間的數據需要提交到fileChannel
            this.fileChannel.position(lastCommittedPosition);//更新fileChannel位置
            this.fileChannel.write(byteBuffer);//寫入到pagecache
            this.committedPosition.set(writePos);//更新committedPosition爲wrotePosition
        } catch (Throwable e) {
            log.error("Error occurred when commit data to FileChannel.", e);
        }
    }
}

至此broker啓動和broker運行寫入消息,消息刷盤已經寫完,但是對於文章最開始的幾個位置點,需要重新說明下,這幾個位置點很容易導致迷惑。

4.2.5.MappedFileQueue與MappedFile的幾個位置說明

MappedFileQueue.flushedWhere  刷盤位置
MappedFileQueue.committedWhere 數據提交位置

MappedFile.wrotePosition  寫位置
MappedFile.committedPosition  提交位置
MappedFile.flushedPosition 刷盤位置

看着中文含義,感覺不到區別,先看圖,此圖前面已經貼過了,再貼一次

再強調下,MappedFile對應一個個具體的文件,比如commitlog物理文件,但是MappedFileQueue對應的是一組物理文件,它們的關係就如圖所示,wrotePosition,committedPosition,flushedPosition都是相對於單個物理文件來說的,而flushedWhere,committedWhere是相對於這一組物理文件來說的,以commitlog文件爲例,每個MappedFile的區間範圍是[(N-1)*1024^3~N*1024^3],其中N表示是第幾個文件,而MappedFileQueue的區間範圍是[0~N*1024^3]。這一組物理文件也可以用commitlog/consumequeue來表達,因爲它們也都包裝了MappedFileQueue。比如圖中的offset就是相對於MappedFileQueue來說的,根據offset就可以得出具體所處的單個物理文件MappedFile,計算方式如下(offset/1024^3) - (mappedFile1.fileFromOffset/1024^) 即爲該offset所屬的MappedFile在MappedFileQueue的索引位置。

說下這幾個位置的變化:

producer發送消息到broker,broker收到消息吧消息寫入到MappedFile,則更新MappedFile.wrotePosition+=msglen。

case1:在同步刷盤和異步刷盤(未開啓transientStorePoolEnable=true情況),刷盤線程吧待刷新數據刷新到磁盤內保存,更新MappedFile.flushedPosition=MappedFile.wrotePosition,更新MappedFileQueue.flushedWhere=MappedFile.fileFromOffset+MappedFile.flushedPosition,該情況下MappedFile.committedPosition、MappedFileQueue.committedWhere是無意義的。

case2:在異步刷盤(開啓transientStorePoolEnable=true情況),提交線程吧待提交數據從堆外內存提交到pagecache保存,更新MappedFile.committedPosition=MappedFile.wrotePosition,MappedFileQueue.committedWhere=MappedFile.fileFromOffset+MappedFile.committedPosition,提交線喚醒異步刷盤線程,刷盤線程更新MappedFile.flushedPosition=MappedFile.committedPosition,更新MappedFileQueue.flushedWhere=MappedFile.fileFromOffset+MappedFile.flushedPosition

broker啓動過程中

在commitlog load中,每個MappedFile的wrotePosition,committedPosition,flushedPosition都被更新爲各自的文件size,即文件末尾。這樣話broker關閉再啓動,之前MappedFile可能只寫了一半數據,再次啓動,該MappedFile就不會再被寫入,轉而新鍵MappedFile進行寫入,這樣做就避免了在刷盤時候漏刷消息到磁盤,雖然浪費了些空間。

在recover中,MappedFileQueue.flushedWhere、committedWhere被恢復爲lastMappedFile.fileFromOffset+commitlog文件中最大的消息位置(即broker關閉之前的MappedFileQueue.flushedWhere)。

 

5.commitlog內消息轉儲到consumequeue、indexfile

轉儲是在服務線程ReputMessageService內進行,該服務線程隨broker啓動而啓動,如下堆棧

該服務線程每次休眠1ms,接着繼續執行轉儲,這裏休眠1ms是避免cps空淪陷導致100%,具體執行邏輯在org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService.doReput()內,代碼如圖

private void doReput() {
    for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {

        //省略不重要代碼

        // 獲取從reputFromOffset%1024^3開始到最後一個MappedFile的wrotePotision的數據引用
        SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
        if (result != null) {
            try {
                this.reputFromOffset = result.getStartOffset();

                //每讀一輪,消息前4字節表示消息總長度,按消息存儲結構讀取,如果還有剩餘的就繼續讀
                for (int readSize = 0; readSize < result.getSize() && doNext; ) {
                	 // 讀取一條消息,把消息包裝爲DispatchRequest
                    DispatchRequest dispatchRequest =
                        DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
                    int size = dispatchRequest.getMsgSize();

                    if (dispatchRequest.isSuccess()) {
                        if (size > 0) {
                        	// 成功讀取Message,更新ConsumeQueue裏的位置信息,更新IndexFile
                            DefaultMessageStore.this.doDispatch(dispatchRequest);

                            if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
                                && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {//如果是主broker且開啓長輪詢(broker默認開啓長輪詢),則通知阻塞的拉取線程進行拉取消息消費,這個是針對消費者消費但是暫時沒有可消費的消息情況,即PULL_NOT_FOUND情況
                                DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
                                    dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
                                    dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
                                    dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
                            }

                            this.reputFromOffset += size;//更新ReputMessageService轉儲commitlog消息的位置
                            readSize += size;
                            //省略不重要代碼
                        } else if (size == 0) {// 讀取到commitlog文件尾,則把this.reputFromOffset更新爲下個commitlog的文件名(即起始位置)
                            this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
                            readSize = result.getSize();
                        }
                    } else if (!dispatchRequest.isSuccess()) {

                        if (size > 0) {
                            log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
                            this.reputFromOffset += size;//讀取到的消息size>0,但是消息非法,那麼跳過該消息,讀取下條消息進行轉儲
                        } else {//消費不正確且size==0,跳出轉儲流程
                            doNext = false;
                            if (DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
                                log.error("[BUG]the master dispatch message to consume queue error, COMMITLOG OFFSET: {}",
                                    this.reputFromOffset);

                                this.reputFromOffset += result.getSize() - readSize;//在消息讀取失敗時候,把this.reputFromOffset更新爲最初的this.reputFromOffset+result.getSize(),跳過消息非法的這部分commitlog數據,並且結束for循環讀取消息進行轉儲
                            }
                        }
                    }
                }//for end
            } finally {
                result.release();
            }
        } else {
            doNext = false;
        }
    }//外層for end
}

該方法的基本思想就是每次循環從commitlog讀取一條消息,然後該該消息轉儲到consumequeue、indexfile。

具體的轉儲是在

public void doDispatch(DispatchRequest req) {
    for (CommitLogDispatcher dispatcher : this.dispatcherList) {// [CommitLogDispatcherCalcBitMap,CommitLogDispatcherBuildConsumeQueue,CommitLogDispatcherBuildIndex]
        dispatcher.dispatch(req);
    }
}

step1:CommitLogDispatcherCalcBitMap.dispatch(DispatchRequest),默認不開啓enableCalcFilterBitMap,忽略。

step2:CommitLogDispatcherBuildConsumeQueue.dispatch(DispatchRequest),轉儲消息的offset到consumequeue。

step3:CommitLogDispatcherBuildIndex.dispatch(DispatchRequest)轉儲消息的index到indexfile,供查詢。

5.1.轉儲消息offset到consumequeue,consumequeue刷盤

在org.apache.rocketmq.store.DefaultMessageStore.CommitLogDispatcherBuildConsumeQueue.dispatch(DispatchRequest)內執行,具體如下

public void dispatch(DispatchRequest request) {
    final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
    switch (tranType) {
        case MessageSysFlag.TRANSACTION_NOT_TYPE:	// 非事務消息
        case MessageSysFlag.TRANSACTION_COMMIT_TYPE:	// 事務消息COMMIT
        	// 非事務消息 或 事務提交消息 建立 消息位置信息 到 ConsumeQueue
            DefaultMessageStore.this.putMessagePositionInfo(request);
            break;
        case MessageSysFlag.TRANSACTION_PREPARED_TYPE:	// 事務消息PREPARED 
        case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:	// 事務消息ROLLBACK
            break;
    }
}

根據消息的sysflag來進行轉儲,如果是事務消息的prepared和rollback消息,則不進行轉儲,這也說明了爲什麼事務消息無法被消費者消費的原因,因爲事務並沒有保存到consumequeue,而消費者是從consumequeue進行消費的。

消息轉儲邏輯見代碼

public void putMessagePositionInfoWrapper(DispatchRequest request) {
    final int maxRetries = 30;
    boolean canWrite = this.defaultMessageStore.getRunningFlags().isCQWriteable();//true
    for (int i = 0; i < maxRetries && canWrite; i++) {//最大重試30次,重試間隔時間是1s
        long tagsCode = request.getTagsCode();
        //省略不重要代碼
        boolean result = this.putMessagePositionInfo(request.getCommitLogOffset(),
            request.getMsgSize(), tagsCode, request.getConsumeQueueOffset());//把消息寫入到cq
        if (result) {//消息offset轉儲到consumequeue成功
        	//消息offset轉儲到consumequeue成功,吧消息時間戳保存到存盤檢測點logicsMsgTimestamp,供broker異常關閉啓動時候恢復
            this.defaultMessageStore.getStoreCheckpoint().setLogicsMsgTimestamp(request.getStoreTimestamp());
            return;
        } else {//消息offset轉儲到consumequeue失敗,則暫停1s接着繼續進行轉儲該消息
            // XXX: warn and notify me
            log.warn("[BUG]put commit log position info to " + topic + ":" + queueId + " " + request.getCommitLogOffset()
                + " failed, retry " + i + " times");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.warn("", e);
            }
        }
    }

    // XXX: warn and notify me
    log.error("[BUG]consume queue can not write, {} {}", this.topic, this.queueId);
    this.defaultMessageStore.getRunningFlags().makeLogicsQueueError();
}
/*
 * 傳入參數 offset是消息在整組commitlog文件中的位置,size即消息長度,tagsCode即消息的hashcode,cqOffset是消息在consumequeue的位置。
 * offset和cqOffset區別,offset是消息在整組commitlog文件中的位置,cqOffset是整組consumequeue文件中的位置,即該topic下該queueID這個整組consumequeue文件中的位置,該值是消息保存到commitlog中從緩存獲取的
 * 功能就是把消息offset+size+hashcode追加到consumerqueue文件內,即追加到consumequeue文件的pagecache內
 */
private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
    final long cqOffset) {

	// 如果已經轉儲過,直接返回成功
    if (offset <= this.maxPhysicOffset) {
        return true;
    }
    // 寫入位置信息到byteBuffer
    this.byteBufferIndex.flip();
    this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
    ////按照consumequeue格式8+4+8存儲
    this.byteBufferIndex.putLong(offset);
    this.byteBufferIndex.putInt(size);
    this.byteBufferIndex.putLong(tagsCode);
    // 計算consumeQueue存儲位置,並獲得對應的MappedFile
    final long expectLogicOffset = cqOffset * CQ_STORE_UNIT_SIZE;

    MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);//獲取expectLogicOffset位置對應的具體mappedfile,如果不存在,則創建
    if (mappedFile != null) {
    	// 當是ConsumeQueue第一個MappedFile && 隊列位置非第一個 && MappedFile未寫入內容,則填充前置空白佔位
        if (mappedFile.isFirstCreateInQueue() && cqOffset != 0 && mappedFile.getWrotePosition() == 0) {
            this.minLogicOffset = expectLogicOffset;
            this.mappedFileQueue.setFlushedWhere(expectLogicOffset);
            this.mappedFileQueue.setCommittedWhere(expectLogicOffset);
            this.fillPreBlank(mappedFile, expectLogicOffset);
            log.info("fill pre blank space " + mappedFile.getFileName() + " " + expectLogicOffset + " "
                + mappedFile.getWrotePosition());
        }

        if (cqOffset != 0) {
            long currentLogicOffset = mappedFile.getWrotePosition() + mappedFile.getFileFromOffset();//相對於該組consumequeue文件的位置

            //expectLogicOffset正常情況應該是等於currentLogicOffset
            if (expectLogicOffset < currentLogicOffset) {//會發生重複轉儲,因此不再進行轉儲
                log.warn("Build  consume queue repeatedly, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
                    expectLogicOffset, currentLogicOffset, this.topic, this.queueId, expectLogicOffset - currentLogicOffset);
                return true;
            }

            if (expectLogicOffset != currentLogicOffset) {
                LOG_ERROR.warn(
                    "[BUG]logic queue order maybe wrong, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
                    expectLogicOffset,
                    currentLogicOffset,
                    this.topic,
                    this.queueId,
                    expectLogicOffset - currentLogicOffset
                );
            }
        }
        this.maxPhysicOffset = offset;//更新爲消息在整組commitlog文件中的位置
        return mappedFile.appendMessage(this.byteBufferIndex.array());//把消息offset+size+hashcode追加到consumerqueue文件內,即追加到pagecache
    }
    return false;
}

消息轉儲到consumeque和消息保存到commitlog類似,都是最終保存到pagecache,那麼既然保存到了pagecache,自然就有刷新到磁盤的操作,在FlushConsumeQueueService服務線程內進行刷盤,最終也是調用org.apache.rocketmq.store.MappedFileQueue.flush(int)進行刷盤,這個和commitlog異步刷盤基本相同,前面commitlog刷盤明白了,這個自然也明白,consumequeue默認是待刷盤數據滿足操作系統頁2頁才刷盤,但是如果超過最大時間,也會強制刷盤,同commitlog異步刷盤。

5.2.保存消息index到indexfile,indexfile刷盤

保存消息的index到indexfile的入口是CommitLogDispatcherBuildIndex.dispatch(DispatchRequest),前面也說過,indexfile對應一個具體的MappedFile,而IndexService是對應整組MappedFile(即整組indexfile)。

indexfile文件格式

indexfile文件size=40+4*500w+20*2000w=420000040字節 約400M

添加消息索引的關鍵方法是putkey,看下面代碼

/*
 * 把keyhash 物理offset存儲到indexfile上,即存儲到MappedFile的pagecache上
 */
public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    if (this.indexHeader.getIndexCount() < this.indexNum) {//已使用的index數目<2000w,則寫入,否則說明indexfile被寫滿了
        int keyHash = indexKeyHashMethod(key);//獲取key的hashcode
        int slotPos = keyHash % this.hashSlotNum;//取模獲取該key要落於哪個slot上
        int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;//計算該索引在indexfile內的slot絕對位置

        FileLock fileLock = null;

        try {

            // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
            // false);
        	//更新indexfile是ReputMessageService單線程操作,無需加鎖
            int slotValue = this.mappedByteBuffer.getInt(absSlotPos);//獲取該key在indexfile絕對位置上的slot值
            if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
                slotValue = invalidIndex;
            }

            long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();//消息在commitlog存儲的時間戳與indexfile第一條消息索引時間戳之間的時間差

            timeDiff = timeDiff / 1000;

            if (this.indexHeader.getBeginTimestamp() <= 0) {//說明該條消息前沒有在該indexfile存儲索引,因此該條消息即是該indexfile的第一條索引記錄
                timeDiff = 0;
            } else if (timeDiff > Integer.MAX_VALUE) {
                timeDiff = Integer.MAX_VALUE;
            } else if (timeDiff < 0) {//說明broker異常關閉啓動後新消息的時間戳小於該indexfile的第一條消息時間戳,可能重複創建索引
                timeDiff = 0;
            }

            int absIndexPos =
                IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
                    + this.indexHeader.getIndexCount() * indexSize;//計算該索引要存放在indexfile上索引區域的絕對位置

            //按照索引格式更新索引位置  20字節
            this.mappedByteBuffer.putInt(absIndexPos, keyHash);//存儲hashcode
            this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);//接着存儲消息在commitlog的物理偏移量
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);//消息的落盤時間與header裏的beginTimestamp的差值(爲了節省存儲空間,如果直接存message的落盤時間就得8bytes)
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);//如果沒有hash衝突,slotValue爲0,如果hash衝突(即多個index的hash落在同一個slot內),

            //更新slot位置 4字節。這個是難理解地方,也是核心地方,slot上存放的是當前索引的個數,那麼進行查找的時候根據keyhash定位到slot後,獲取了slotValue,該值就是索引數量,那麼可以根據該值獲取該key對應的索引在indexfile上索引的真正位置
            this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());//代碼@1

            if (this.indexHeader.getIndexCount() <= 1) {
                this.indexHeader.setBeginPhyOffset(phyOffset);
                this.indexHeader.setBeginTimestamp(storeTimestamp);
            }

            this.indexHeader.incHashSlotCount();//已使用的hashslot數量+1,這個沒具體什麼意義
            this.indexHeader.incIndexCount();//已經使用的index數量+1,用於判斷indexfile是否被寫滿
            this.indexHeader.setEndPhyOffset(phyOffset);//更新indexheader的結束物理位置爲最新的消息的物理位置,每次刷新一條索引,都會更新
            this.indexHeader.setEndTimestamp(storeTimestamp);//更新indexheader的結束時間戳爲最新的消息的存儲時間戳,每次刷新一條索引,都會更新

            return true;
        } catch (Exception e) {
            log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
        } finally {
            if (fileLock != null) {//fileLock爲null,不進入
                try {
                    fileLock.release();
                } catch (IOException e) {
                    log.error("Failed to release the lock", e);
                }
            }
        }
    } else {
        log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
            + "; index max num = " + this.indexNum);
    }

    return false;
}

本質就是把消息keyhash、消息在commitlog的物理偏移量、時間戳存放到MappedFile的pagecache上,難點是代碼@1處,該處代碼是核心,解釋如下:

代碼@1處是把索引區域的slotvalue更新爲該keyhash落於的hash槽的值,這個理解麻煩些,更新爲這個有什麼用呢?既然是通過hash來選擇落於哪個slot內,那麼必然存在hash衝突,更新爲該值爲對應slot上存放的是當前索引的個數,那麼進行查找的時候根據keyhash定位到slot後,獲取了slotValue,該值就是索引數量indexCount,那麼可以根據indexCount該值獲取該key對應的索引在indexfile上索引的真正位置,公式就是absIndexPos=40+4*500w+indexCount*20,那麼在進行查詢的時候,先根據key得到slotvalue即indexCount,繼而計算出absIndexPos,繼而獲取absIndexPos的keyhash和查詢的keyHash比較,相同則匹配,接着獲取該索引位置的前一個preslotvalue,即preindexCount進行遍歷,獲取keyhash等同查詢keyHash的值,這樣看起來就像個鏈表結構,這個設計也很巧妙,使用索引數量連接衝突的hashkey。

具體根據消息key查詢是org.apache.rocketmq.store.index.IndexFile.selectPhyOffset(List<Long>, String, int, long, long, boolean)通過rocketmq-console上面的查詢發送QUERY_MESSAGE命令查詢,broker對應的處理器是QueryMessageProcessor,broker處理堆棧如圖

那麼有沒有查詢時候slotvalue爲0的情況,當然有,爲0說明該key不存在或者暫時沒有被ReputMessageService服務線程刷新消息的索引到indexfile文件,這樣情況就返回結果是null了。

org.apache.rocketmq.store.index.IndexFile.selectPhyOffset(List<Long>, String, int, long, long, boolean)代碼加註釋如下

/*
 * 根據topic+key+開始時間戳+結束時間戳+最大查詢條數來查詢消息,查詢到的消息phyOffset保存到集合phyOffsets
 */
public void selectPhyOffset(final List<Long> phyOffsets, final String key, final int maxNum,
    final long begin, final long end, boolean lock) {
    if (this.mappedFile.hold()) {
        int keyHash = indexKeyHashMethod(key);
        int slotPos = keyHash % this.hashSlotNum;
        int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;//獲取待查詢的key所歸屬的slot絕對位置

        //省略不重要代碼
            if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()
                || this.indexHeader.getIndexCount() <= 1) {
            	//爲slotValue==0說明該key不存在或者暫時沒有被ReputMessageService服務線程刷新消息的索引到indexfile文件,這樣情況就返回結果是null了。
            } else {
                for (int nextIndexToRead = slotValue; ; ) {
                    if (phyOffsets.size() >= maxNum) {
                        break;
                    }

                    int absIndexPos =
                        IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
                            + nextIndexToRead * indexSize;

                    int keyHashRead = this.mappedByteBuffer.getInt(absIndexPos);//獲取keyhash
                    long phyOffsetRead = this.mappedByteBuffer.getLong(absIndexPos + 4);//獲取消息在commitlog上的物理偏移量

                    long timeDiff = (long) this.mappedByteBuffer.getInt(absIndexPos + 4 + 8);//時間戳
                    int prevIndexRead = this.mappedByteBuffer.getInt(absIndexPos + 4 + 8 + 4);//即前preIndexCount

                    if (timeDiff < 0) {
                        break;
                    }

                    timeDiff *= 1000L;

                    long timeRead = this.indexHeader.getBeginTimestamp() + timeDiff;
                    boolean timeMatched = (timeRead >= begin) && (timeRead <= end);

                    if (keyHash == keyHashRead && timeMatched) {//hash相同且時間匹配,吧該消息在commitlog上的物理偏移量保存到phyOffsets集合
                        phyOffsets.add(phyOffsetRead);
                    }

                    if (prevIndexRead <= invalidIndex
                        || prevIndexRead > this.indexHeader.getIndexCount()
                        || prevIndexRead == nextIndexToRead || timeRead < begin) {//preSlotvalue爲0退出循環
                        break;
                    }

                    //prevIndexRead!=0則說明該slot位置有hash衝突,需要繼續根據prevIndexRead進行查詢
                    nextIndexToRead = prevIndexRead;
                }
            }
        //省略不重要代碼
    }
}

在ReputMessageService服務線程中把消息key和offset保存到indexfile,即保存到MappedFile的pagecache屬性,但是並沒有保存到磁盤,具體是在哪裏保存到磁盤呢?indexfile刷盤和commitlog、consumequeue刷盤是不同的,broker並沒有針對indexfile刷盤起一個線程服務用於刷盤indexfile,爲什麼呢?因爲一個indexfile可以保存2000w個消息索引,不像commitlog、consumequeue需要頻繁刷盤保存消息到磁盤防止系統崩潰丟失。indexfile的刷盤最終操作是org.apache.rocketmq.store.index.IndexFile.flush(),該操作的調用堆棧如圖

而org.apache.rocketmq.store.index.IndexService.getAndCreateLastIndexFile()的調用堆棧如圖

原來是ReputMessageService線程轉儲消息offset、key到indexfile的時候,如果當前indexfile滿了,則新建一個indexfile,把寫滿的indexfile進行刷盤保存。這樣做也有風險,如果服務器宕機了,那麼當前indexfile的索引就全部丟失了,單身indexfile只是用於查詢,不是重要的消息數據,丟失可以容忍。實際上查詢消息在indexfile丟失情況下,可以直接通過grep "消息hex字符串" commitlog文件 這樣方法可以搜索到的。

 

----------------------------------分割線-----------------------------

至此吧broker啓動、broker接收producer消息到消息存儲和刷盤。commitlog、consumequeue、indexfile與MappedFileQueue、MappedFile的關係說明白了,平時自己都是記錄筆記,沒有寫博客習慣,以後養成。第一次寫博客,寫的太長了,博客應該短些好。

參考:丁威 《rocketmq技術內幕》,閱讀源碼參考這個書效果很好。

最後貼個大圖,broker啓動的流程圖

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章