RMQ主要存储的文件包括commitlog文件、consumeQueue文件、IndexFile文件。
CommitLog是消息存储文件,所有消息主题的消息都存储在CommitLog文件中;ConsumeQueue是消息消费队列文件,消息达到commitlog文件后将被异步转发到消息消费队列,供消息消费者消费;IndexFile是消息索引文件,主要存储的是key和offset的对应关系。
RocketMQ的数据流向如下图所示:
DefaultMessageStore是消息存储中最重要的一个类,包含了大量对存储文件操作的API,可以说其他模块要想对消息进行操作都必须经过DefaultMessageStore类。
private final MessageStoreConfig messageStoreConfig; //消息存储配置属性
private final CommitLog commitLog; //commitLog文件的存储实现类
//消息队列存储缓存表,按消息主题分组
private final ConcurrentMap> consumeQueueTable;
private final FlushConsumeQueueService flushConsumeQueueService;//:消息队列文件 ConsumeQueue刷盘线程
private final CleanCommitLogService cleanCommitLogService; //清除CommitLog文件服务
private final CleanConsumeQueueService cleanConsumeQueueService;//清除ConsumeQueue文件服务
private final IndexService indexService; //索引文件实现类
private final AllocateMappedFileService allocateMappedFileService;//Mapped分配服务
private final ReputMessageService reputMessageService; //CommitLog消息分发
private final HAService haService; //存储HA机制
private final TransientStorePool transientStorePool; //消息堆内存缓存
private final RunningFlags runningFlags = new RunningFlags();
private final SystemClock systemClock = new SystemClock();
private final ScheduledExecutorService scheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("StoreScheduledThread"));
private final BrokerStatsManager brokerStatsManager;
//消息拉取长轮询模式消息达到监听器
private final MessageArrivingListener messageArrivingListener;
private final BrokerConfig brokerConfig; //Broker配置属性
//文件刷盘监测点
private StoreCheckpoint storeCheckpoint;
private AtomicLong printTimes = new AtomicLong(0);
//CommitLog文件转发请求
private final LinkedList dispatcherList;
消息发送存储的追踪可以DefaultMessageStore的putMessage方法开始,一步步Debug之后,我们用一张图来描述消息发送存储的流程:
RocketMQ通过使用内存映射文件来提高IO的访问性能,无论是CommitLog、ConsumeQueue还是IndexFile,单个文件都被设计为固定长度,如果一个文件写满以后就再创建一个新文件,文件名就为该文件第一条消息对应的全局物理偏移量。
MappedFile是封装存储文件的关键类,也是内存映射文件的具体实现,核心属性如下:
public static final int OS_PAGE_SIZE = 1024 * 4; //操作系统每页大小,默认4k
//当前JVM实例中MappedFile虚拟内存
private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
//当前JVM实例中MappedFile对象个数
private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
//当前文件的写指针,从0开始
protected final AtomicInteger wrotePosition = new AtomicInteger(0);
//当前文件的提交指针
protected final AtomicInteger committedPosition = new AtomicInteger(0);
//刷写到磁盘指针,该指针之前的数据持久化到磁盘中
private final AtomicInteger flushedPosition = new AtomicInteger(0);
protected int fileSize; //文件大小
protected FileChannel fileChannel; //文件通道
protected ByteBuffer writeBuffer = null; //堆内存ByteBuffer
//堆内存池
protected TransientStorePool transientStorePool = null;
private String fileName; //文件名称
private long fileFromOffset; //文件初始偏移量
private File file; //物理文件
private MappedByteBuffer mappedByteBuffer; //物理文件对应的内存映射
private volatile long storeTimestamp = 0; //文件最后一次内容写入时间
//是否是MappedFileQueue队列中第一个文件
private boolean firstCreateInQueue = false;
MappedFileQueuede是MappedFile的管理容器,使用CopyOnWriteArrayList管理MappedFile,封装了commitLog的存储目录
private final String storePath; //存储目录
private final int mappedFileSize; //单个文件的存储大小
//MappedFile文件集合
private final CopyOnWriteArrayList mappedFiles = new CopyOnWriteArrayList();
//创建MappedFile服务类
private final AllocateMappedFileService allocateMappedFileService;
private long flushedWhere = 0; //当前刷盘指针,表示该指针之前的所有数据持久化到磁盘
private long committedWhere = 0; //当前数据提交指针,内存中ByteBuffer的写指针,该值大于等于flushedWhere
临时存储池。RocketMQ单独创建一个MappedByteMapper内存缓存池,用来临时存储数据,数据先写入该内存映射中,然后由commit线程定时将数据从该内存复制到与目的物理文件对应的内存映射中。
private final int poolSize; //可用Buffer的个数
private final int fileSize; //每个By特Buffer的大小
private final Deque availableBuffers; //Buffer容器,双端队列
private final MessageStoreConfig storeConfig; //配置
RocketMQ的存储文件(CommitLog、ConsumeQueue、IndexFile)都被设计为固定长度,如果一个文件写满以后再创建一个新文件,文件名就为该文件第一条消息对应的全局物理偏移量。
1)、首先获取当前commitlog目录的最小偏移量:首先获取目录下的第一个文件,如果该文件可用,则返回该文件的起始偏移量,否则返回下一个文件的起始偏移量;
2)、根据该offset返回下一个文件的起始偏移量:后获取一个文件的大小,减去(offset%MappedFileSize)其目的是回到下一文件的起始偏移量;
3)、根据偏移量与消息长度查找消息:首先根据偏移找到所在的物理偏移量,然后用offset与文件长度取余得到在文件内的偏移量,从该偏移量读取size长度的内容返回即可。如果只根据消息便宜查找消息,则首先找到文件内的偏移量,然后尝试读取4个字节获取消息的实际长度,最后读取指定字节即可。
ComsumeQueue可以看成是CommitLog关于消息消费的“索引”文件,ConsumeQueue的第一级目录为消息主题,第二级目录为主题的消息队列:
为了加速ConsumeQueue消息条目的检索速度与节省磁盘空间,ConsumeQueue中的存储单元是一个20 字节定长的数据,包含8字节的commitlogoffset(这条消息在commitlog文件的实际偏移量)、4字节的消息大小、以及消息的hash值
为了提高根据主题与消息队列检索消息的速度,RocketMQ引入了hash所以机制为消息建立索引,这里借用了HashMap的思想:
1)、IndexHeader头部,包含40个字节,记录该IndexFile的统计信息,其结构如下:
2)、Hash槽,一个IndexFile默认包含500万个Hash槽,每个hash槽存储的是落在该hash槽的hashcode最新的Index的索引
3)、Index条目列表,默认一个索引文件包含2000万个条目,每一个Index条目结构如下:
checkpoint的作业是记录commitlog、ConsumeQueue、index文件的刷盘时间点,文件固定长度为4k,其中只用该文件的前面24个字节