生产者发送一条消息的时候,在主broker里面最终会运行到:
PutMessageResult result = this.commitLog.putMessage(msg); 在这里第一次有了锁,也就是发送一条消息,一路行都没有锁,直到这里涉及mappedfile的时候才有锁。因为要保证消息有序性质,先到的消息的offset更低,所以这里必须要有锁。
对于commit-log来说,首先要找一个存储介质,也就是从MappedFileQueue里面取出一个Queue,如果没有那么需要构造一个出来,构造MappedFile的过程都在AllocateMappedFileService里面。
AllocateMappedFileService也是一个单独线程,他在主循环的mmapOperation里面,阻塞在req = this.requestQueue.take();
private PriorityBlockingQueue<AllocateRequest> requestQueue = new PriorityBlockingQueue<AllocateRequest>();
他是一个优先阻塞队列,当外面有任务提交request的时候这个线程才会从阻塞中醒来。通过优先队列可以保证文件创建的先后顺序。
对于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);
mappedByteBuffer就是跟磁盘文件共享存储的,并且是基于直接内存的,但是写在内存里面还需要做一次force操作才能刷到磁盘里面去。
对于一个mappedfile来说,第一个mappedfile的文件名字就叫0000000000000,第二个交0000000001073741824,第一个mappedfile的大小就是0000000001073741824
fileFromOffSet就是文件名字。wrotePosition是已经写到内存的位置,flushedPosition是已经刷盘到硬盘的位置。后面两个都是以这个mapped为起点的,不是绝对全局0作为起点
在doAppend方法里面完成了对于生产者发送过来的消息的存储,其中涉及到这条消息的绝对offset,绝对offset其实就是fileFromOffSet+wrotePosition。
还涉及topicQueueTable:private HashMap<String/* topic-queueid */, Long/* offset */> topicQueueTable = new HashMap<String, Long>(1024);
也就是一个topic、queue-id作为key,value是从0开始依次递增。
完成对mappedfile的写入以后,锁就可以释放了,后面还有两个关于可靠性的两个同步问题,磁盘同步跟主备同步。
handleDiskFlush(result, putMessageResult, msg);
handleHA(result, putMessageResult, msg);
commit-log里面维护的是mappedfileQueue,它的刷盘的逻辑是:
public boolean flush(final int flushLeastPages) {
boolean result = true;
MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, this.flushedWhere == 0);
if (mappedFile != null) {
long tmpTimeStamp = mappedFile.getStoreTimestamp();
int offset = mappedFile.flush(flushLeastPages);
long where = mappedFile.getFileFromOffset() + offset;
result = where == this.flushedWhere;
this.flushedWhere = where;
if (0 == flushLeastPages) {
this.storeTimestamp = tmpTimeStamp;
}
}
return result;
}
根据上次刷盘位置取出最后一次刷盘的mappedFile,执行它的flush方法,其实就是直接执行force即可刷盘。
public int 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.
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();
}
能否刷盘需要看isAbleToFlush:
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;
}
也就是刷盘的位置比写入到mappedfile内存的位置小的时候,就需要进行刷盘了。这里面的flushLeastPages其实就是等到数据积攒到比较大的时候再一次性刷盘,这个参数默认是4,只有经过一段时间以后才变成0。也就是平时都是攒着,超过一定时间才不考虑攒不攒的问题。
int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
int flushPhysicQueueThoroughInterval =
CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();
boolean printFlushProgress = false;
// Print flush progress
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
this.lastFlushTimestamp = currentTimeMillis;
flushPhysicQueueLeastPages = 0;
printFlushProgress = (printTimes++ % 10) == 0;
}
还有一点就是MappedFile还是继承自ReferenceResource,后者也只有前者这么一个子类。由于mappedfile涉及直接内存,所以需要我们自己去进行释放、维护引用计数,每次要用到mappedfile里面数据的时候,都会hold一次防止被回收。如果计数到达0,那么进入到clean方法操作直接内存的释放。
后面的handle-ha前面已经提过不说了,还有涉及到reputMessageService关于index文件、consumeQueue的操作这里暂且不提。