RocketMQ源码深度解析一之消息存储

前言:RocketMQ的消息持久化是基于文件系统,而从效率来看文件系统>kv存储>关系型数据库。那么,到底是如何存储的,相信对源码进行解析,将会是我们大大提高对消息存储的认识。

(一)存储层的整体结构
首先看下结构图
这里写图片描述
对于我们业务层来说,都是通过DefaultMessageStore类做为操作入口。RocketMQ下主要有6类文件,分别是三类大文件:Index文件,consumequeue文件,commitlog文件。三类小文件:checkpoint文件,config目录下的配置文件.和abort。
而对于三类大文件,使用的就是nio的MappedByteBuffer类来提高读写性能。这个类是文件内存映射的相关类,支持随机读和顺序写。在RocketMQ中,被封装成了MappedFile类。
RocketMQ对于每类大文件,在存储时候分割成了多个固定大小的文件,每个文件名为前面所有的文件大小加1(也就是偏移量)。从而实现对整个大文件的串联拼接。接下来就需要看看MappedFIle这个封装类了。

(二)MappedFile类
对于 commitlog、 consumequeue、 index 三类大文件进行磁盘读写操作,均是通过 MapedFile 类来完成。这个类相当于MappedByteBuffer的包装类。
(1)主要成员变量

    //默认页大小为4k
    public static final int OS_PAGE_SIZE = 1024 * 4;
    protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
    //JVM中映射的虚拟内存总大小  
    private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
    //JVM中mmap的数量  
    private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
    //当前写文件的位置
    protected final AtomicInteger wrotePosition = new AtomicInteger(0);
    //ADD BY ChenYang
    protected final AtomicInteger committedPosition = new AtomicInteger(0);
    private final AtomicInteger flushedPosition = new AtomicInteger(0);
    //映射文件的大小
    protected int fileSize;
    //映射的fileChannel对象
    protected FileChannel fileChannel;
    /**
     * Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
     */
    protected ByteBuffer writeBuffer = null;
    protected TransientStorePool transientStorePool = null;
    //映射的文件名
    private String fileName;
    //映射的起始偏移量
    private long fileFromOffset;
    //映射的文件
    private File file;
    //映射的内存对象
    private MappedByteBuffer mappedByteBuffer;
    //最后一条消息保存时间  
    private volatile long storeTimestamp = 0;
    //是不是刚刚创建的
    private boolean firstCreateInQueue = false;

(2)文件顺序写操作
这里提供了两种顺序写的方法。
第一个是提供给commitlog使用的,传入消息内容,然后CommitLog按照规定的格式构造二进制信息并顺序写,方法是appendMessage(final Object msg, final AppendMessageCallback cb);这里的msg是MessageExtBrokerInner或者MessageExtBatch,具体的写操作是在回调类的doAppend方法进行。最后再调用appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb);

第二个是appendMessage(final byte[] data)方法

public boolean appendMessage(final byte[] data) {
        //找出当前要的写入位置
        int currentPos = this.wrotePosition.get();
        //如果当前位置加上要写入的数据大小小于等于文件大小,则说明剩余空间足够写入。
        if ((currentPos + data.length) <= this.fileSize) {
            try {
                //则由内存对象 mappedByteBuffer 创建一个指向同一块内存的
                //ByteBuffer 对象,并将内存对象的写入指针指向写入位置;然后将该二进制信
                //息写入该内存对象,同时将 wrotePostion 值增加消息的大小;
                this.fileChannel.position(currentPos);
                this.fileChannel.write(ByteBuffer.wrap(data));
            } catch (Throwable e) {
                log.error("Error occurred when append message to mappedFile.", e);
            }
            this.wrotePosition.addAndGet(data.length);
            return true;
        }

(3)消息刷盘操作
主要的方法是commit(final int flushLeastPages);

public int commit(final int commitLeastPages) {
        if (writeBuffer == null) {
            //no need to commit data to file channel, so just regard wrotePosition as committedPosition.
            return this.wrotePosition.get();
        }
        if (this.isAbleToCommit(commitLeastPages)) {
            if (this.hold()) {
                commit0(commitLeastPages);
                this.release();
            } 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()) {
            this.transientStorePool.returnBuffer(writeBuffer);
            this.writeBuffer = null;
        }

        return this.committedPosition.get();
    }

解析:
(1)首先判断文件是否已经写满类,即wrotePosition等于fileSize,若写慢则进行刷盘操作
(2)检测内存中尚未刷盘的消息页数是否大于最小刷盘页数,不够页数也暂时不刷盘。
(3)MappedFile的父类是ReferenceResource,该父类作用是记录MappedFile中的引用次数,为正表示资源可用,刷盘前加一,然后将wrotePosotion的值赋给committedPosition,再减一。

(4)随机读操作

public SelectMappedBufferResult selectMappedBuffer(int pos) {
        int readPosition = getReadPosition();
        if (pos < readPosition && pos >= 0) {
            if (this.hold()) {
                ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
                byteBuffer.position(pos);
                int size = readPosition - pos;
                ByteBuffer byteBufferNew = byteBuffer.slice();
                byteBufferNew.limit(size);
                return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
            }
        }

        return null;
    }

随机读的操作有两个方法,上面显示第一种方法,读取从指定位置开始的所有消息,海有一种是读取指定位置开始的指定消息大小的消息内容。这两个方法均是调用 ByteBuffer 的 slice 和 limit 方法获取消息内容,然后初始化 SelectMapedBufferResult 对象并返回;该对象的 startOffset 变量是读取消息的开始位置加上该文件的起始偏移量;

(二)MappedFileQueue
我们在应用层中访问commitlog和consumequeue是通过MappFileQueue来操作MappedFile类的,从而间接操作磁盘上面的文件,MappFileQueue是多个MappedFile组成的队列。接下来解读一下其重要的方法
(1)获取在指定时间点后更新的文件

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];

解析:遍历MappedFile,若遇到的文件更新时间大于指定时间,则返回该对象,若找不到则返回最后一个MappedFile对象

(2)清理指定偏移量所在文件之后的文件

public void truncateDirtyFiles(long offset) {
        List<MappedFile> willRemoveFiles = new ArrayList<MappedFile>();

        for (MappedFile file : this.mappedFiles) {
            long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize;
            if (fileTailOffset > offset) {
                if (offset >= file.getFileFromOffset()) {
                    file.setWrotePosition((int) (offset % this.mappedFileSize));
                    file.setCommittedPosition((int) (offset % this.mappedFileSize));
                    file.setFlushedPosition((int) (offset % this.mappedFileSize));
                } else {
                    file.destroy(1000);
                    willRemoveFiles.add(file);
                }
            }
        }

        this.deleteExpiredFile(willRemoveFiles);
    }

解析:遍历整个列表,每个MappedFile对象对应着一个固定大小的文件,当当文件的起始偏移量fileFromOffset<=offset<=fileFromOffset+fileSize,则表示指定的位置偏移量 offset 落在的该文件上,则将对应的 MapedFile 对象的 wrotepostion 和commitPosition 设置为 offset%fileSize,若文件的起始偏移量fileFromOffset>offset,即是命中的文件之后的文件,则将这些文件删除并且从 MappFileQueue 的 MapedFile 列表中清除掉。

(3)获取或创建最后一个文件

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值