RocketMQ中写入的消息会存储到commitlog文件下,然后再异步转存到consumequeue以及index文件,它们的关系如下:

CommitLog
commitlog中存放很多的mappedFile文件,当前Broker中的所有消息都是落盘到这些mappedFile文件。mappedFile文件默认最大为1G,文件名是20位10进制数构成,表示当前文件的第一条消息起始偏移量。每一个mappedFile文件物理上是被拆分的,但是逻辑上是连续的。
消息单元
一个Broker中只会有一个commitlog目录,Broker接收到的所以消息都会顺序存放进mappedFile,不管你是什么Topic消息,也就是说Broker中存放消息时是没有按照Topic进行分类存放的,那是如何区分这些消息是哪个Queue,哪个Topic的呢
consumequeue
刚刚讲到mappedFile是用于存放消息的各种信息,从这些信息可以知道当前消息属于那个Queue那个Topic,那么如果通过遍历mappedFile去获取消息是相当麻烦的事情
为了提高效率,RocketMQ在为每个Topic在~/store/consumequeue中创建一个目录,目录名称就是Topic的名称,在该Topic下会为每个Queue创建独立的目录,目录名为QueueId,每个目录中存放着若干的consumequeue文件,consumequeue是commitlog的索引文件,可以通过consumequeue定位到commitlog的具体消息。
indexFile
除了通过正常的指定Topic进行消息消费外,RocketMQ还提供了一种根据key进行消息查询的功能,该查询通过store目录中的index子目录中的indexFile进行索引实现查询的,当Broker收到包含key的消息时这个消息索引就会被写入indexFile,如果消息没key不会写入。
MappedFile
CommitLog与ConsumeQueue都有成员变量MappedFileQueue,MappedFileQueue的定义如下:
public class MappedFileQueue {
/**
* 批量删除文件上限
*/
private static final int DELETE_FILES_BATCH_MAX = 10;
/**
* 目录
*/
private final String storePath;
/**
* 每个映射文件大小
*/
private final int mappedFileSize;
/**
* 映射文件数组
*/
private final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<>();
/**
* TODO
*/
private final AllocateMappedFileService allocateMappedFileService;
/**
* 最后flush到的位置offset
*/
private long flushedWhere = 0;
/**
* 最后commit到的位置offset,这个变量是针对所有MappedFile的,committedWhere/mappedFileSize = 写入的文件的序号,从0开始
*/
private long committedWhere = 0;
/**
* 最后store时间戳
*/
private volatile long storeTimestamp = 0;
MappedFileQueue含有变量mappedFiles,它是一个MappedFile的数组,MappedFile是实际保存消息的地方。MappedFile的定义如下:
public class MappedFile extends ReferenceResource {
/**
* 文件系统缓存里内存页的最小分配单元,4K
*/
public static final int OS_PAGE_SIZE = 1024 * 4;
/**
* 映射虚拟内存总字节数
*/
private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
/**
* 映射文件总数
*/
private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
/**
* 当前追加到MappedByteBuffer的位置
*/
protected final AtomicInteger wrotePosition = new AtomicInteger(0);
/**
* ADD BY ChenYang
* 当前commit位置
*/
protected final AtomicInteger committedPosition = new AtomicInteger(0);
/**
* 当前flush位置
*/
private final AtomicInteger flushedPosition = new AtomicInteger(0);
/**
* 文件大小
*/
protected int fileSize;
/**
* fileChannel
* {@link #file}的channel = new RandomAccessFile(this.file, "rw").getChannel()
*/
protected FileChannel fileChannel;
/**
* Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
* 写入缓冲
*/
protected ByteBuffer writeBuffer = null;
/**
* writeBuffer缓存池
*/
protected TransientStorePool transientStorePool = null;
/**
* 文件名
*/
private String fileName;
/**
* 文件开始的offset。
* 目前文件名即offset
*/
private long fileFromOffset;
/**
* 文件
*/
private File file;
/**
* 文件映射Buffer
*/
private MappedByteBuffer mappedByteBuffer;
/**
* 最后插入数据时间。即{@link #mappedByteBuffer}变更时间
*/
private volatile long storeTimestamp = 0;
/**
* 是否最先创建在队列
* {@link MappedFileQueue#getLastMappedFile(long, boolean)}
*/
private boolean firstCreateInQueue = false;
MappedFile初始化
MappedFile先将数据存储到堆外内存,然后通过commit线程将数据提交到内存映射Buffer中,再通过Flush线程将内存映射Buffer中的数据持久化到磁盘。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);
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提交
MappedFile先将数据写入到内存ByteBuffer writeBuffer中,然后每隔一段时间将writeBuffer中的数据提交到内存映射MappedByteBuffer mappedByteBuffer。在CommitLog中有个线程CommitRealTimeService会实时检查MappedFile是否满足commit的条件:Commit需要缓冲区内至少含有4页数据,也就是16KB,或者是最近200毫秒内没有消息Commit。
MappedFile的commit流程
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());
}
}
// 所以数据提交后,清空缓存
if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
this.transientStorePool.returnBuffer(writeBuffer);
this.writeBuffer = null;
}
return this.committedPosition.get();
}
MappedFile#isAbleToCommit
/**
* 是否能够commit。满足如下条件任意条件:
* 1. 映射文件已经写满
* 2. commitLeastPages > 0 && 未commit部分超过commitLeastPages
* 3. commitLeastPages = 0 && 有新写入部分
*
* @param commitLeastPages commit最小分页
* @return 是否能够写入
*/
protected boolean isAbleToCommit(final int commitLeastPages) {
int commit = this.committedPosition.get();
int write = this.wrotePosition.get();
if (this.isFull()) {
return true;
}
if (commitLeastPages > 0) {
return ((write / OS_PAGE_SIZE) - (commit / OS_PAGE_SIZE)) >= commitLeastPages;
}
return write > commit;
}
判断是否执行commit操作:
1)如果文件已满返回true;
2)如果commitLeastPages 大于0,且脏数据页大于commitLeastPages则返回true;
3)如果commitLeastPages 小于等于0,则只要有脏数据及提交
MappedFile#commit0
/**
* commit实现,将writeBuffer写入fileChannel,更新committedPosition = wrotePosition
*
* @param commitLeastPages commit最小页数。用不上该参数
*/
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 byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
// 写入fileChannel
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer);
// 设置position
this.committedPosition.set(writePos);
} catch (Throwable e) {
log.error("Error occurred when commit data to FileChannel.", e);
}
}
}
具体提交的实现,首先创建writeBuffer的共享存储区,然后将position回退到上一次提交的位置lastCommittedPosition,设置limit为wrotePosition(当前最大有效数据位置),然后把wrotePosition到lastCommittedPosition之间的数据写入到fileChannel中。总之,commit的作用就是将MappedFile中的writeBuffer中的数据提交到文件通道FileChannel中。
MappedFile刷写磁盘
刷写磁盘,就是调用MappedByteBuffer或者FileChannel中的force方法将内存中的数据写入到磁盘中。流程如下:

/**
* 如果满足flush条件,返回这次flush后的位置
* 如果不满住flush条件,返回上次flush的位置
*
* @param flushLeastPages flush最小页数
* @return The current flushed position
*/
public int flush(final int flushLeastPages) {
//是否达到刷盘条件
if (this.isAbleToFlush(flushLeastPages)) {
//加锁,同步刷盘
if (this.hold()) {
//获得读指针
int value = getReadPosition();
try {
//数据从writeBuffer提交到fileChannel再刷盘
if (writeBuffer != null || this.fileChannel.position() != 0) {
this.fileChannel.force(false);
} else {
//从mmap刷新数据到磁盘
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();
}

95

被折叠的 条评论
为什么被折叠?



