消息入盘的基本逻辑都在MappedFile这个类中。根据配置的不同,MappedFile为我们准备了两块不同的缓存。
- DirectBuffer类型的writeBuffer
- MappedByteBuffer类型的mappedByteBuffer
如果writeBuffer不为空的情况下,优先使用writeBuffer,否则数据将写入到mappedByteBuffer。数据只会写入一块buffer,不会同时写入。
首先,我们看看什么情况下会使用writeBuffer。
public boolean isTransientStorePoolEnable() {
return transientStorePoolEnable && FlushDiskType.ASYNC_FLUSH == getFlushDiskType()
&& BrokerRole.SLAVE != getBrokerRole();
}
由此可见,只有broker主节点,配置为异步刷盘的情况下,才有可能会使用writeBuffer。
第一步,将消息写入MappedFile的缓存区域,这步进行之后,会更新wrotePosition字段。
//选择缓冲内存
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
...
//将消息封写入缓存内存
byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
第二步,将内存缓存的数据刷入磁盘。这步分为三种情况:
- 同步 + MappedByteBuffer
- 异步 + MappedByteBuffer
- 异步 + DirectBuffer
第一种情况对应的逻辑在GroupCommitService类中,核心方法为GroupCommitService::doCommit,最终调用到public int MappedFile::flush(final int flushLeastPages)。
第二种情况对应的逻辑在FlushRealTimeService类中,核心方法为FlushRealTimeService::run,最终也是调用到最终调用到public int MappedFile::flush(final int flushLeastPages)。
第三种情况对应的逻辑在CommitRealTimeService中,核心方法为CommitRealTimeService::run,最终调用到protected void MappedFile::commit0(final int commitLeastPages)。
其实第一种和第二种基本一致,唯一的区别在于,第一种是有数据就刷盘,第二种是当数据达到一定的数量(2页)才刷盘。
分别贴一下两个方法的核心代码。
/**
* 第一种情况,参数为0,有数据就会刷盘。
* 第二种情况,参数为页数(2),达到指定大小才会刷盘
*/
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;
}
/**
* @return The current flushed position
*/
public int MappedFile::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.
//这个writeBuffer的判空很魔幻。目前的版本,走到这里的情况下,writeBuffer永远为空。如果不为空了,这个地方应该是需要修改的,加上将writeBuffer写到fileChannel的逻辑
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();
}
这个执行完成之后,flushedPosition会更新。
protected void MappedFile::commit0(final int commitLeastPages) {
int writePos = this.wrotePosition.get();
int lastCommittedPosition = this.committedPosition.get();
if (writePos - this.committedPosition.get() > 0) {
try {
//将DirectBuffer中内容写入文件
ByteBuffer byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer);
//更新committedPosition字段
this.committedPosition.set(writePos);
} catch (Throwable e) {
log.error("Error occurred when commit data to FileChannel.", e);
}
}
}
这个执行完成之后,commitedPosition会更新。
最后,附上时序图。消息写入内存缓存的时序
第一、二种情况,消息刷入磁盘的时序
第三种情况,消息刷入磁盘的时序
参考文章:
https://blog.csdn.net/yankunhaha/article/details/97232870
源码学习的其他方面的,有兴趣了解的可以看看:
https://blog.csdn.net/wusheng520/article/details/108798042