8.rocketmq源代码学习---服务端消息存储结构

在了解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存储单元
CommitLogOffset8byte消息在CommitLog中的文件位置
size4byte消息大小
tag hash8byte消息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存储单元
totalSize4byte消息在文件中占用的长度
magicCode4byte
bodyCrc4byte
queueId4byte所在队列id
flag4byte消息标志,系统不做干预,完全由应用决定如何使用
queueOffset8byte队列中的offset
physicalOffset8byte文件中的位置//fileFromOffset + byteBuffer.position()
sysFlag8byte是否压缩内容、是否事务消息
bornTimestamp8byte客户端消息产生的时间
bornHost8byte客户端的ip地址
storeTimestamp8byte消息开始存储的时间
storeHostAddress8byte接收消息的broker host
reconsumeTimes4byte消息被重复消费的次数
prepared transaction offset8byte
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查询,这个索引文件比较复杂,再单独一章节来讲…

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值