RocketMQ消息存储结构简介--CommitLog

RocketMQ消息存储是整个系统的核心,直接决定着吞吐性能和高可用性。RocketMQ存储消息并没有借助oracle、mysql等关系型数据库,而是直接操作文件。借助java NIO的力量,使得I/O性能十分高。当消息来的时候,顺序写入CommitLog。为了Consumer消费消息的时候,能够方便的根据topic查询消息,在CommitLog的基础上衍生出了CosumerQueue文件,存放了某topic的消息在CommitLog中的偏移位置。此外为了支持根据消息key查询消息,还构建了index文件。这三个文件(逻辑上是三个),就是RocketMQ的主要存储内容,大致结构如下图所示(图片来自书籍《RocketMQ技术内幕》):
在这里插入图片描述
限于篇幅,这篇文章仅介绍CommitLog相关的一些东西。

CommitLog类、MappedFileQueue、MappedFile

CommitLog名字取得非常好,除了消息本身,它记录了消息的方方面面的信息,通过一条CommitLog我们可以还原出很多东西。例如消息是何时、由哪个producer发送的,被发送到了哪个消息队列,属于哪个topic,有哪些属性等等。RokcetMQ存储的消息其实存储的就是这个CommitLog记录。后面的叙述中,如果没有特别说明,我们可以将CommitLog记录等同于消息,而CommitLog特指存储消息的文件。

CommitLog类如下所示:

public class CommitLog {
    // Message's MAGIC CODE daa320a7
    public final static int MESSAGE_MAGIC_CODE = 0xAABBCCDD ^ 1880681586 + 8;
    // End of file empty MAGIC CODE cbd43194
    private final static int BLANK_MAGIC_CODE = 0xBBCCDDEE ^ 1880681586 + 8;
   
    private final MappedFileQueue mappedFileQueue; // 存放的文件队列

    // 省略代码
}    

CommitLog类属性很多,但是最重要的是mappedFileQueue属性。之前我们一直说消息最终存储在CommitLog里,实际上CommitLog是一个逻辑上的概念。真正的文件是一个个MappedFile,然后组成了mappedFileQueue。一个MappedFile最多能存放1G的CommitLog,这个大小在MessageStoreConfi类里面定义了的:
在这里插入图片描述
当一个MappedFile写满了之后,就会创建第二MappedFile,然后继续存CommitLog,如下所示:
在这里插入图片描述
MappedFileQueue是存放MappedFile的队列结构,如下所示:

public class MappedFileQueue {
    private static final int DELETE_FILES_BATCH_MAX = 10;

    private final String storePath; // 文件存储路径,例如/home/gameloft9/store/commitlog

    private final int mappedFileSize; // 单个MappedFile大小,默认1G

    private final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<MappedFile>(); // mappedFile列表

    private final AllocateMappedFileService allocateMappedFileService; // 分配mappedFile的service
    
    // 省略代码
 }   

MappedFileQueue存放了一个个MappedFile,然后还记录了一些额外的信息,例如存储文件路径、单个MappedFile大小等等。
下面我们再来看MappedFile的结构:

public class MappedFile extends ReferenceResource {
    protected int fileSize;
    protected FileChannel fileChannel; // 读写通道
    /**
     * Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
     */
    protected ByteBuffer writeBuffer = null; // 缓冲区,使用的是直接堆外内存
    
    private String fileName;
    private File file;
    
    private MappedByteBuffer mappedByteBuffer; // 内存映射
}

如果之前没有接触过FileChannel、ByteBuffer、MappedByteBuffer,可以先补习一下这篇文章:ByteBuffer介绍
writeBuffer使用的是堆外内存,mappedByteBuffer是直接将文件映射到内存中,两者的使用是互斥的。如果启用了临时缓冲池(默认不启用),那么就会使用writeBuffer写commitlog,否则就是mappedBtyeBuffer写commitlog。
CommitLog、MappedFileQueue和MappedFile的大致关系如下所示:
在这里插入图片描述

CommitLog记录

一条CommitLog记录包括哪些内容呢?CommitLog要实现的功能,决定了它需要存储哪些内容。首先要实现消息的存储,肯定需要把消息存下来。其次,为了方便创建ConsumerQueue,需要记录topic、queueId等信息。为了能跟踪消息,需要记录消息发送方地址、发送时间等。。。
完整的CommitLog记录如下所示:
在这里插入图片描述
为什么会先跳过消息存储流程先讲存储内容结构呢?因为流程实在是太复杂、内容太多了,很容易晕头转向。如果先对存储的内容有一个大致的概念,后面再理解消息存储过程会好很多。骨头难啃,总要挑一处简单的下嘴嘛。下面从功能上解释为什么要存这些字段。不了解整个流程,理解这些字段也会有些困难,我会尽量保证通俗易懂。

TotalSize

TotalSize很好理解,就是整个CommitLog记录的大小,包括上面列出来的所有字段大小。TotalSize占用4个字节,在往byteBuffer写CommitLog的时候,首先就会写入这个CommitLog大小,如下所示:

 // 1 TOTALSIZE
 this.msgStoreItemMemory.putInt(msgLen);
MagicCode

MagicCode是一个特殊的字段,它可以标志ByteBuffer中的某个CommitLog是一个正常的CommitLog,还是因为ByteBuffer没有多余的空间存放该CommitLog,导致该CommitLog是一个空的CommitLog。

MagicCode有两个值,如下所示:

    // Message's MAGIC CODE daa320a7
    public final static int MESSAGE_MAGIC_CODE = 0xAABBCCDD ^ 1880681586 + 8;
    // End of file empty MAGIC CODE cbd43194
    private final static int BLANK_MAGIC_CODE = 0xBBCCDDEE ^ 1880681586 + 8;

MESSAGE_MAGIC_CODE表明该CommitLog记录是一条正常的记录,BLANK_MAGIC_CODE表明该CommitLog记录是一个空的CommitLog记录。

如果存储CommitLog发现空间不够,会马上开辟第二个文件重新存储CommitLog记录,但是之前的空的CommitLog也一样会保存下来。在Broker正常退出或者异常退出,重启之后需要恢复Broker的时候,就会根据这个MagicCode判断该条CommitLog是否是正常的。

BodyCRC

学过计算机网络的人一定知道CRC,CRC即循环冗余校验码,是数据通信领域中最常用的一种查错校验码,通过CRC就可以知道数据的正确性和完整性。RocketMQ通过CRC来校验消息部分,如下所示:

if (checkCRC) {
    int crc = UtilAll.crc32(bytesContent, 0, bodyLen);
    if (crc != bodyCRC) {
       log.warn("CRC check failed. bodyCRC={}, currentCRC={}", crc, bodyCRC);
       return new DispatchRequest(-1, false/* success */);
    }
}
queueId

queueId很熟悉的,就是消息发往哪个队列。queueId在producer发送消息时会选择出来,这个在文章RocketMQ发送消息时如何选择队列中已经很详细的讲过了,这里就不再赘述queueId是如何产生的了。
Topic下会有一堆消息队列(ConsumerQueue),RocketMQ在保存完消息后,会随后构建ConsumerQueue,里面存放着Topic下消息的在CommitLog文件中的偏移量,方便根据Topic查询消费消息。ConsumerQueue的构建、消息的消费都是重点内容,会在单独的文章中进行介绍。

Flag

暂时不知道有什么用,默认值是0。

QueueOffset

我们之前讲过,为了方便Consumer能根据Topic快速的查询消息,在CommitLog的基础上构建了ConsumerQueue,里面存放了某个Topic下面的所有消息在CommitLog中的位置。

同样的,这里的QueueOffset存放了消息记录应该在ConsumerQueue中的位置,这样构建ConsumerQueue的时候,就知道该条记录在ConsummerQueue的位置顺序,在消费消息的时候很有用处。QueueOffset一般是是累加1的,如下所示:

case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
     // The next update ConsumeQueue information
     CommitLog.this.topicQueueTable.put(key, ++queueOffset);
      break;

这个与ConsumerQueue的存储结构有关,后面介绍ConsumerQueue存储结构的时候会涉及到。

PhysicalOffset

这个很简单了,就是消息在CommitLog中的物理位置。需要注意的是,我们CommitLog对应着磁盘上的多个文件,这里的偏移量不是从某个文件开始算的,而是从第一个文件偏移开始算起的。

SysFlag

SysFlag是RocketMQ内部使用的标记位,通过位运算进行标记。例如是否对消息进行了压缩、是否属于事务消息。SysFlag初始值为0,可与下面的标记进行位运算。

    public final static int COMPRESSED_FLAG = 0x1;
    public final static int MULTI_TAGS_FLAG = 0x1 << 1; // 2
    public final static int TRANSACTION_NOT_TYPE = 0; // 不参与位运算,用作结果比较,表示无事务
    public final static int TRANSACTION_PREPARED_TYPE = 0x1 << 2; // 4
    public final static int TRANSACTION_COMMIT_TYPE = 0x2 << 2; // 8
    public final static int TRANSACTION_ROLLBACK_TYPE = 0x3 << 2; // 12

例如对消息进行了压缩,那么SysFlag = 0 | 0x1。又例如flag & TRANSACTION_ROLLBACK_TYPE,可以判断消息是否是事务消息,如果等于0说明不是事务消息。

BornTimestamp

Producer发送消息的时间,如下所示:

 requestHeader.setBornTimestamp(System.currentTimeMillis());
BornHost

Producer发送消息使用的套接字地址

 msgInner.setBornHost(ctx.channel().remoteAddress());

CommitLog存的时候是读取4个字节的rao ip + 4个字节的端口号:

 byteBuffer.put(inetSocketAddress.getAddress().getAddress(), 0, 4);
 byteBuffer.putInt(inetSocketAddress.getPort());
StoreTimestamp

消息在broker上存储时间。

StoreHostAddress

Broker的套接字地址,存储方式同BornHost。

ReconsumeTimes

重复消费次数,初始为0。我们消费消息的时候,如果发生异常,可以选择晚一点重新消费,如下所示:

consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(
                    List<MessageExt> msgs, ConsumeConcurrentlyContext context) {

                try{
                    for(MessageExt msg : msgs){
                        if(msg.getTopic().equals("test")){
                            log.info("收到test类型消息:" + new String(msg.getBody()));
                        }
                    }

                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // 消费成功
                }catch(Exception e){
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER; // 稍后重新消费
                }
            }
        });

Broker重试的时候,这个ReconsumeTimes就会+1,默认最大重试次数是16次。

PreparedTransactionOffset

事务消息相关的一个属性。RocketMQ事务消息基于两阶段提交,这里仅仅了解一点就够了,涉及到事务消息的时候会再提到。

Body

消息体,没什么好说的。需要注意的是,Body前面其实会有4字节(int)的Body长度,这里没有画出来。

Topic

主题,没什么好说的。需要注意的是,Topic前面其实会有1字节(byte)的Topic长度,这里没有画出来。

Properties

消息属性。需要注意的是,Properties前面其实会有2字节(short)的Properties长度,这里没有画出来。
Properties既存放了RocketMQ内部用到的一些属性,也存放了用户的一些属性。例如发送消息的TAG就存放在Properties里面:

   Message msg = new Message("test",// topic
                            "TagB",// tag就会存放在Properties里面
                            ("我发了一条消息").getBytes());// body

Properties中的一些常用key都定义在了MessageConstant里面,如下所示:

public class MessageConst {
    public static final String PROPERTY_KEYS = "KEYS";
    public static final String PROPERTY_TAGS = "TAGS";
    public static final String PROPERTY_WAIT_STORE_MSG_OK = "WAIT";
    public static final String PROPERTY_DELAY_TIME_LEVEL = "DELAY";
    public static final String PROPERTY_RETRY_TOPIC = "RETRY_TOPIC";
    public static final String PROPERTY_REAL_TOPIC = "REAL_TOPIC";
    public static final String PROPERTY_REAL_QUEUE_ID = "REAL_QID";
    public static final String PROPERTY_TRANSACTION_PREPARED = "TRAN_MSG";
    public static final String PROPERTY_PRODUCER_GROUP = "PGROUP";
    public static final String PROPERTY_MIN_OFFSET = "MIN_OFFSET";
    public static final String PROPERTY_MAX_OFFSET = "MAX_OFFSET";
    public static final String PROPERTY_BUYER_ID = "BUYER_ID";
    public static final String PROPERTY_ORIGIN_MESSAGE_ID = "ORIGIN_MESSAGE_ID";
    public static final String PROPERTY_TRANSFER_FLAG = "TRANSFER_FLAG";
    public static final String PROPERTY_CORRECTION_FLAG = "CORRECTION_FLAG";
    public static final String PROPERTY_MQ2_FLAG = "MQ2_FLAG";
    public static final String PROPERTY_RECONSUME_TIME = "RECONSUME_TIME";
    public static final String PROPERTY_MSG_REGION = "MSG_REGION";
    public static final String PROPERTY_TRACE_SWITCH = "TRACE_ON";
    public static final String PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX = "UNIQ_KEY";
    public static final String PROPERTY_MAX_RECONSUME_TIMES = "MAX_RECONSUME_TIMES";
    public static final String PROPERTY_CONSUME_START_TIMESTAMP = "CONSUME_START_TIME";
}

小结

通过了解CommitLog记录的一些属性,可以帮助我们更好的了解RocketMQ消息存储、消费的一些细节。

发布了379 篇原创文章 · 获赞 85 · 访问量 59万+

猜你喜欢

转载自blog.csdn.net/GAMEloft9/article/details/100562191