rocketmq里面生产者发送一次消息以后commit-log做了哪些事情

生产者发送一条消息的时候,在主broker里面最终会运行到:

  

PutMessageResult result = this.commitLog.putMessage(msg); 在这里第一次有了锁,也就是发送一条消息,一路行都没有锁,直到这里涉及mappedfile的时候才有锁。因为要保证消息有序性质,先到的消息的offset更低,所以这里必须要有锁。

对于commit-log来说,首先要找一个存储介质,也就是从MappedFileQueue里面取出一个Queue,如果没有那么需要构造一个出来,构造MappedFile的过程都在AllocateMappedFileService里面。

AllocateMappedFileService也是一个单独线程,他在主循环的mmapOperation里面,阻塞在req = this.requestQueue.take();

private PriorityBlockingQueue<AllocateRequest> requestQueue =    new PriorityBlockingQueue<AllocateRequest>();

他是一个优先阻塞队列,当外面有任务提交request的时候这个线程才会从阻塞中醒来。通过优先队列可以保证文件创建的先后顺序。

对于mappedfile的初始化:

    private void init(final String fileName, final int fileSize) throws IOException {
        this.fileName = fileName;
        this.fileSize = fileSize;
        this.file = new File(fileName);
        this.fileFromOffset = Long.parseLong(this.file.getName());
        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);

  

mappedByteBuffer就是跟磁盘文件共享存储的,并且是基于直接内存的,但是写在内存里面还需要做一次force操作才能刷到磁盘里面去。

对于一个mappedfile来说,第一个mappedfile的文件名字就叫0000000000000,第二个交0000000001073741824,第一个mappedfile的大小就是0000000001073741824

fileFromOffSet就是文件名字。wrotePosition是已经写到内存的位置,flushedPosition是已经刷盘到硬盘的位置。后面两个都是以这个mapped为起点的,不是绝对全局0作为起点

在doAppend方法里面完成了对于生产者发送过来的消息的存储,其中涉及到这条消息的绝对offset,绝对offset其实就是fileFromOffSet+wrotePosition。

还涉及topicQueueTable:private HashMap<String/* topic-queueid */, Long/* offset */> topicQueueTable = new HashMap<String, Long>(1024);

也就是一个topic、queue-id作为key,value是从0开始依次递增。

完成对mappedfile的写入以后,锁就可以释放了,后面还有两个关于可靠性的两个同步问题,磁盘同步跟主备同步。

handleDiskFlush(result, putMessageResult, msg);
handleHA(result, putMessageResult, msg);

commit-log里面维护的是mappedfileQueue,它的刷盘的逻辑是:

    public boolean flush(final int flushLeastPages) {
        boolean result = true;
        MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, this.flushedWhere == 0);
        if (mappedFile != null) {
            long tmpTimeStamp = mappedFile.getStoreTimestamp();
            int offset = mappedFile.flush(flushLeastPages);
            long where = mappedFile.getFileFromOffset() + offset;
            result = where == this.flushedWhere;
            this.flushedWhere = where;
            if (0 == flushLeastPages) {
                this.storeTimestamp = tmpTimeStamp;
            }
        }

        return result;
    }

  根据上次刷盘位置取出最后一次刷盘的mappedFile,执行它的flush方法,其实就是直接执行force即可刷盘。

    public int flush(final int flushLeastPages) {
        if (this.isAbleToFlush(flushLeastPages)) {
            if (this.hold()) {
                int value = getReadPosition();

                try {
                    //We only append data to fileChannel or mappedByteBuffer, never both.
                    if (writeBuffer != null || this.fileChannel.position() != 0) {
                        this.fileChannel.force(false);
                    } else {
                        this.mappedByteBuffer.force();
                    }
                } catch (Throwable e) {
                    log.error("Error occurred when force data to disk.", e);
                }

                this.flushedPosition.set(value);
                this.release();
            } else {
                log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
                this.flushedPosition.set(getReadPosition());
            }
        }
        return this.getFlushedPosition();
    }

  

能否刷盘需要看isAbleToFlush:

    private boolean isAbleToFlush(final int flushLeastPages) {
        int flush = this.flushedPosition.get();
        int write = getReadPosition();

        if (this.isFull()) {
            return true;
        }

        if (flushLeastPages > 0) {
            return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages;
        }

        return write > flush;
    }

  也就是刷盘的位置比写入到mappedfile内存的位置小的时候,就需要进行刷盘了。这里面的flushLeastPages其实就是等到数据积攒到比较大的时候再一次性刷盘,这个参数默认是4,只有经过一段时间以后才变成0。也就是平时都是攒着,超过一定时间才不考虑攒不攒的问题。

  

                int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();

                int flushPhysicQueueThoroughInterval =
                    CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();

                boolean printFlushProgress = false;

                // Print flush progress
                long currentTimeMillis = System.currentTimeMillis();
                if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
                    this.lastFlushTimestamp = currentTimeMillis;
                    flushPhysicQueueLeastPages = 0;
                    printFlushProgress = (printTimes++ % 10) == 0;
                }

  

还有一点就是MappedFile还是继承自ReferenceResource,后者也只有前者这么一个子类。由于mappedfile涉及直接内存,所以需要我们自己去进行释放、维护引用计数,每次要用到mappedfile里面数据的时候,都会hold一次防止被回收。如果计数到达0,那么进入到clean方法操作直接内存的释放。

后面的handle-ha前面已经提过不说了,还有涉及到reputMessageService关于index文件、consumeQueue的操作这里暂且不提。

原文地址:https://www.cnblogs.com/notlate/p/12007242.html