RocketMQ的消息存储
RocketMQ 会创建多个MappedFile用来存储文件,每个MappedFile大小固定,有自己的内存缓冲区和对应的系统文件,所有的MappedFile由CommitLog中的MappedFileQueue统一维护
每个Broker都对应有一个MessageStore,专门用来存储发送到它的消息,不过MessageStore本身不是文件,只是存储的一个抽象,MessageStore 中保存着一个 CommitLog,CommitLog 维护了一个 MappedFileQueue,而MappedFileQueue 中又维护了多个 MappedFile,每个MappedFile都会映射到文件系统中一个文件,这些文件才是真正的存储消息的地方,MappedFile的文件名为它记录的第一条消息的全局物理偏移量
流程图
1.消息发送存储流程
@Override
public PutMessageResult putMessages(MessageExtBatch messageExtBatch) {
//如果当前 Broker 停止工作或 Broker为 SLAVE 角色或当前 Rocket 不支持写入则拒绝消息写入;如果消息主题长度超 256 个字符、消息属性长度超过 65536 个字符将拒绝该消息写人
PutMessageStatus checkStoreStatus = this.checkStoreStatus();
if (checkStoreStatus != PutMessageStatus.PUT_OK) {
return new PutMessageResult(checkStoreStatus, null);
}
//校验topic的长度和消息的大小
PutMessageStatus msgCheckStatus = this.checkMessages(messageExtBatch);
if (msgCheckStatus == PutMessageStatus.MESSAGE_ILLEGAL) {
return new PutMessageResult(msgCheckStatus, null);
}
long beginTime = this.getSystemClock().now();
//把消息放进去commitLog
PutMessageResult result = this.commitLog.putMessages(messageExtBatch);
long elapsedTime = this.getSystemClock().now() - beginTime;
if (elapsedTime > 500) {
log.warn("not in lock elapsed time(ms)={}, bodyLength={}", elapsedTime, messageExtBatch.getBody().length);
}
this.storeStatsService.setPutMessageEntireTimeMax(elapsedTime);
if (null == result || !result.isOk()) {
this.storeStatsService.getPutMessageFailedTimes().incrementAndGet();
}
return result;
}
private PutMessageStatus checkStoreStatus() {
//判断当前broker是否停止工作,拒绝消息写入
if (this.shutdown) {
log.warn("message store has shutdown, so putMessage is forbidden");
return PutMessageStatus.SERVICE_NOT_AVAILABLE;
}
//如果Broker 为SLAVE 角色,拒绝消息写入
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
long value = this.printTimes.getAndIncrement();
if ((value % 50000) == 0) {
log.warn("message store has shutdown, so putMessage is forbidden");
}
return PutMessageStatus.SERVICE_NOT_AVAILABLE;
}
//如果当前rocket不支持写入则拒绝消息写入
if (!this.runningFlags.isWriteable()) {
long value = this.printTimes.getAndIncrement();
if ((value % 50000) == 0) {
log.warn("message store has shutdown, so putMessage is forbidden");
}
return PutMessageStatus.SERVICE_NOT_AVAILABLE;
} else {
this.printTimes.set(0);
}
return PutMessageStatus.PUT_OK;
}
- 如果当前 Broker 停止工作或 Broker为 SLAVE 角色或当前 Rocket 不支持写入则拒绝消息写入;如果消息主题长度超 256 个字符、消息属性长度超过 65536 个字符将拒绝该消息写人
MappedFile unlockMappedFile = null;
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
- 获取当前可以写入 Commitlog 件,Commitlog 文件存储 为${ROCKET_HOME }/ store/commitlog 录,每一个文件默认lG, 一个文件写满后再创建另外一个,以该文件中第一个偏移量为文件名,偏移量小于20 位用0补齐
假设第一个文件初始偏移0 ,第2个文件的1073741824 ,代表该文件中的第一条消息的物理偏移 1073741824 ,这样根据物理偏移量能快速定位到消息. MappedFileQueue可以看作是{ROCKET_HOME }/store/commitlog 文件夹而MappedFile 则对应该文件夹下一个个的文件
//:在写入 CommitLog 之前,先申请 putMessageLock ,也就是将消息存储到CommitLog 文件中是串行的
putMessageLock.lock();
try {
//设置消息的存储时间
messageExtBatch.setStoreTimestamp(beginLockTimestamp);
//如 mappedFile为空,表明$ {ROCKET_HOME}/store/ CommitLog 目录下不存在任何文件,说明本次消息是第一次消息发送
if (null == mappedFile || mappedFile.isFull()) {
//用偏移量为0创建第一个commit文件,文件名0000000000000000
mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
}
//如果创建失败,抛出CREATE_MAPEDFILE_FAILED
if (null == mappedFile) {
log.error("Create mapped file1 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString());
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
}
- 在写入 CommitLog 之前,先申请 putMessageLock ,也就是将消息存储到CommitLog 文件中是串行的.设置消息的存储时间,
如 mappedFile为空,表明$ {ROCKET_HOME}/store/ CommitLog 目录下不存在任何文件,说明本次消息是第一次消息发送,用偏移量为0创建第一个commit文件,文件名0000000000000000,如果创建失败,抛出CREATE_MAPEDFILE_FAILED
//将消息追加到mappedFile中
mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback);
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
//先获取mappedFile当前写指针
int currentPos = this.wrotePosition.get();
//如果currentPos 大于或等于文件大小则说明文件已写满,抛出 AppendMessageStatus.UNKNOWN_ ERROR 如果 currentPos 小于文件大小,通过 slice ()方法创建 一个与 MappedFile 的共存区,并设置 position 为当前指针
if (currentPos < this.fileSize) {
//mappedByteBuffer 即为该apped文件在内存中的映射。
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
byteBuffer.position(currentPos);
AppendMessageResult result;
if (messageExt instanceof MessageExtBrokerInner) {
//MappedFile的文件名 ,byteBuffer=MappedFile的内存缓冲,fileSize - currentPos=当前文件剩余容量
//messageExt 内部封装好的消息
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
} else if (messageExt instanceof MessageExtBatch) {
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
} else {
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
this.wrotePosition.addAndGet(result.getWroteBytes());
this.storeTimestamp = result.getStoreTimestamp();
return result;
}
log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
-
将消息追加到 MappedFile 首先先获取 MappedFile 当前写指针,如果currentPos 大于或等于文件大小则 明文件已写满,抛出 AppendMessageStatus. UNKNOWN_ ERROR, 如果 currentPos 小于文件大小,通过 slice ()方法创建 个与 MappedFile 的共
存区,并设置 position 为当前指针,mappedByteBuffer 即为该文件在内存中的映射。当追加消息到MappedFile中,会优先追加到 writeBuffer中
// :创建全局唯一消息 ID ,消息 ID 有16 字节
long wroteOffset = fileFromOffset + byteBuffer.position();
this.resetByteBuffer(storeHostHolder, storeHostLength);
String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(storeHostHolder), wroteOffset);
//获取该消息在消息队列的偏移量,CommitLog 中保存了当前所有消息队列的当前待写入偏移量
keyBuilder.setLength(0);
keyBuilder.append(msgInner.getTopic());
keyBuilder.append('-');
keyBuilder.append(msgInner.getQueueId());
String key = keyBuilder.toString();
Long queueOffset = CommitLog.this.topicQueueTable.get(key);
if (null == queueOffset) {
queueOffset = 0L;
CommitLog.this.topicQueueTable.put(key, queueOffset);
}
//:如果消息长度+END FILE_ MIN_ BLANK_ LENGTH 大于 CommitLog 文件的空闲空间,则返回 AppendMessageStatus.END_OF _FILE, Broker 会重新创建一个新的CommitLog 文件来存储该消息 从这里可以看出,每个 CommitLog 文件最少会空 个字节,高 字节存储当前文件剩余空间,低 字节存储魔数 CommitLog.BLANK MAGIC CODE
final int msgLen = calMsgLength(msgInner.getSysFlag(), bodyLength, topicLength, propertiesLength);
- 创建全局唯一消息 ID .获取该消息在消息队列的偏移量,CommitLog 中保存了当前所有消息队列的当前待写入偏移量
final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
// Write messages to the queue buffer
byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId,
msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
- 返回追加成功的结果,将消息内容存储到 ByteBuffer 中,然后创建 AppendMessageResult 这里只是将消息存储在 MappedFile 应的内存映射 buffer ,并没有刷写到磁盘