在了解rocketmq服务端消息是怎么存储之前,先来了解下rocketmq消息的结构。
rocketmq在主题下划分出队列(默认4个队列),通过队列来实现消费端的负载均衡,实现消费端的消息拉取。
接下来,了解下rocketmq的存储结构:
先找到rocketmq存储目录,rocketmq配置文件broker.properties里面有2个属性配置消息存储的路径:
storePathRootDir=C:\Users\Administrator\store
storePathCommitLog=C:\Users\Administrator\store\commitlog
这里面每一个文件夹都是存储的什么内容呢:
一、consumequeue文件夹
consumequeue文件夹目录结构为:consumequeue / topicName / queueId0~n
consumequeue文件内容存储结构为:
ConsumeQueue存储单元 | ||
---|---|---|
CommitLogOffset | 8byte | 消息在CommitLog中的文件位置 |
size | 4byte | 消息大小 |
tag hash | 8byte | 消息tag属性的hashCode |
consumequeue中一个主题一个队列单独一个文件
每个存储单元占用的大小是固定的即:8+4+8=20byte
固定大小有什么好处呢?拉取消息的时候,比如客户端告诉服务端,“帮我拉取主题为:TopicA,队列ID为1,第 200 个offset 的数据”,服务端收到后,就可以根据consumequeue文件找到对应的位置即:200*20byte,这样就不需要扫描文件、查找文件,直接根据位置即可返回消息,这样性能就可以大大的提升了。
那么consumequeue中的tag有什么用呢?留到《消息过滤》那一节再去讲解… 这里我们先记下来这个疑问…
详细结构如下:
二、config文件夹
三、CommitLog文件夹
commitLog一个文件为1个G
文件的命名也不是随便定的,见下图:
第一个为0000000000000000000(20位)
第二个为0+102410241024(1g)
第n个为:(n-1)*1g
为什么这么设计呢,是为了方便根据messageId定位,接着往下看…
commitLog存储了真正的消息内容。
CommitLog存储单元 | ||
---|---|---|
totalSize | 4byte | 消息在文件中占用的长度 |
magicCode | 4byte | |
bodyCrc | 4byte | |
queueId | 4byte | 所在队列id |
flag | 4byte | 消息标志,系统不做干预,完全由应用决定如何使用 |
queueOffset | 8byte | 队列中的offset |
physicalOffset | 8byte | 文件中的位置//fileFromOffset + byteBuffer.position() |
sysFlag | 8byte | 是否压缩内容、是否事务消息 |
bornTimestamp | 8byte | 客户端消息产生的时间 |
bornHost | 8byte | 客户端的ip地址 |
storeTimestamp | 8byte | 消息开始存储的时间 |
storeHostAddress | 8byte | 接收消息的broker host |
reconsumeTimes | 4byte | 消息被重复消费的次数 |
prepared transaction offset | 8byte | |
body | 不定长 | 消息内容 |
topic | 不定长 | 消息主题 |
propertyLength | 不定长 | 消息属性长度 |
四、MessageId
这里再提一下MessageId
rocketmq同步发送消息的时候返回了消息id,同样做了非常精妙的设计,可以根据MessageId提取消息内容,是怎么设计的呢?让我们一起来揭开…
来看下生成MessageId的代码:
MessageDecoder.java
public static String createMessageId(final ByteBuffer input, final ByteBuffer addr, final long offset) {
input.flip();
input.limit(MessageDecoder.MSG_ID_LENGTH);
// 消息存储主机地址 IP PORT 8
input.put(addr);
// 消息对应的物理分区 OFFSET 8
input.putLong(offset);
return UtilAll.bytes2string(input.array());//转为16进制
}
这个offset是什么呢,来看下调用messageId的地方:
// fileFromOffset(文件起始位置)+当前文件写入的位置
long wroteOffset = fileFromOffset + byteBuffer.position();
String msgId =
MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(),
wroteOffset);
fileFromOffset是什么呢,就是CommitLog的文件名称,
也就是说根据MessageId,我们知道消息存储在哪个服务器,并且根据MessageId知道存储在哪个文件,并且还知道哪个位置,有了这些信息,我们可以直接定位到消息内容,设计的真的很精妙…
来看下rocketmq-tool.jar,当调用queryMsgById的时候,客户端的处理逻辑:
MQAdminImpl.vierMessage()
public MessageExt viewMessage(String msgId) throws RemotingException, MQBrokerException,
InterruptedException, MQClientException {
try {
MessageId messageId = MessageDecoder.decodeMessageId(msgId);
//根据messageId得到broker的地址,并向对应的broker发送offset
return this.mQClientFactory.getMQClientAPIImpl().viewMessage(
RemotingUtil.socketAddress2String(messageId.getAddress()), messageId.getOffset(), 1000 * 3);
}
catch (UnknownHostException e) {
throw new MQClientException("message id illegal", e);
}
}
再来看看服务端的方法:
QueryMessageProcessor.viewMessageById
public RemotingCommand viewMessageById(ChannelHandlerContext ctx, RemotingCommand request)
throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final ViewMessageRequestHeader requestHeader =
(ViewMessageRequestHeader) request.decodeCommandCustomHeader(ViewMessageRequestHeader.class);
// 由于使用sendfile,所以必须要设置
response.setOpaque(request.getOpaque());
//关键在这里,根据offset获取内容
final SelectMapedBufferResult selectMapedBufferResult =
this.brokerController.getMessageStore().selectOneMessageByOffset(requestHeader.getOffset());
if (selectMapedBufferResult != null) {
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
try {
FileRegion fileRegion =
new OneMessageTransfer(response.encodeHeader(selectMapedBufferResult.getSize()),
selectMapedBufferResult);
ctx.channel().writeAndFlush(fileRegion).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
selectMapedBufferResult.release();
if (!future.isSuccess()) {
log.error("transfer one message by pagecache failed, ", future.cause());
}
}
});
}
catch (Throwable e) {
log.error("", e);
selectMapedBufferResult.release();
}
return null;
}
else {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("can not find message by the offset, " + requestHeader.getOffset());
}
return response;
}
根据offset获取消息内容代码:
CommitLog.selectOneMessageByOffset()
@Override
public SelectMapedBufferResult selectOneMessageByOffset(long commitLogOffset) {
//根据offset获取消息内容,谜底马上揭晓,继续往下看
SelectMapedBufferResult sbr = this.commitLog.getMessage(commitLogOffset, 4);
if (null != sbr) {
try {
// 1 TOTALSIZE
int size = sbr.getByteBuffer().getInt();
return this.commitLog.getMessage(commitLogOffset, size);
}
finally {
sbr.release();
}
}
return null;
}
根据offset获取消息内容:
public SelectMapedBufferResult getMessage(final long offset, final int size) {
int mapedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMapedFileSizeCommitLog();
//根据offset先获取到文件
MapedFile mapedFile = this.mapedFileQueue.findMapedFileByOffset(offset, (0 == offset ? true : false));
if (mapedFile != null) {
//再根据offset找到位置,获取内容
int pos = (int) (offset % mapedFileSize);
SelectMapedBufferResult result = mapedFile.selectMapedBuffer(pos, size);
return result;
}
return null;
}
别慌别慌,马上就到了,最后一个
MapedFileQueu.findMapedFileByOffset()
public MapedFile findMapedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
try {
this.readWriteLock.readLock().lock();
MapedFile mapedFile = this.getFirstMapedFile();
if (mapedFile != null) {
int index =
(int) ((offset / this.mapedFileSize) - (mapedFile.getFileFromOffset() / this.mapedFileSize));
if (index < 0 || index >= this.mapedFiles.size()) {
logError
.warn(
"findMapedFileByOffset offset not matched, request Offset: {}, index: {}, mapedFileSize: {}, mapedFiles count: {}, StackTrace: {}",//
offset,//
index,//
this.mapedFileSize,//
this.mapedFiles.size(),//
UtilAll.currentStackTrace());
}
try {
return this.mapedFiles.get(index);
}
catch (Exception e) {
if (returnFirstOnNotFound) {
return mapedFile;
}
}
}
}
catch (Exception e) {
log.error("findMapedFileByOffset Exception", e);
}
finally {
this.readWriteLock.readLock().unlock();
}
return null;
}
其实代码很简单… 就是一个取余… 设计的非常精妙,代码其实很简单… 好的设计反而越简单啊!
五、Index文件夹
当发送消息时,设置了message的key属性,则会存储在index文件夹中,方便根据key查询,这个索引文件比较复杂,再单独一章节来讲…