1.从源码角度看RocketMQ普通消息生产的过程

598 阅读7分钟

本文已参与[新人创作礼]活动,一路开启掘金创作之路。

1.客户端发送消息

1.1 生产者实例 DefaultMQProducer

基于rocketmq-4.9.0 版本分析rocketmq 发送方式有三种

  • 同步发送
  • 异步发送
  • 单向发送 这里以同步发送为例,这些内容不是本次讲解的重点,如果想系统学习RocketMq(MQ)可以参考我的脑图 rocketmq脑图
```
public class SyncProducer {

    public static void main(String[] args) throws Exception {

        // 实例化消息生产者Producer
        DefaultMQProducer producer = new DefaultMQProducer("qiuguan-p-group");
        // 设置NameServer的地址
        producer.setNamesrvAddr("127.0.0.1:9876");

        // 启动Producer实例
        producer.start();

        for (int i = 0; i < 5; i++) {
            // 创建消息,并指定Topic,Tag和消息体
            Message msg = new Message("test-topic",
                    "*",
                    ("Hello RocketMQ-" + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            //指定消息key, 消费者可以根据key做幂等消费
            msg.setKeys("key-unique-" + i);

            //同步发送
            SendResult sendResult = producer.send(msg);

            // 通过sendResult返回消息是否成功送达
            System.out.printf("%s%s%n", sendResult, i);
        }


        // 如果不再发送消息,关闭Producer实例。
        producer.shutdown();
    }
}
```

发送消息的大致流程如下:

  1. 根据topic从nameServer获取路由表信息
  2. 选择一个MessageQueue(在broker模式下手动创建默认是4个)
    2.1 轮询算法(默认)
    2.2 最小投递延迟算法
  3. 构建消息体 SendMessageRequestHeader对象(设置topic, queueid, 以及将TAGS设置到properties中等等)
  4. 构建远程netty请求命令(这里关注一下,后面服务端接收消息就是根据这个)
RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader)
  1. 发送消息

2.服务端接收消息并存储消息

2.1 服务端接收消息

前面说了客户端发送消息时会构建netty请求命令,那么服务端是怎么定义的呢?当broker(BrokerController)启动时会调用初始化 initialize()方法, 其中内部会注册很多处理器registerProcessor(),其中就有发送消息,拉取消息的处理器

public void registerProcessor() {
    /**
     * SendMessageProcessor
     * 生产者客户端发送消息处理器
     */
    SendMessageProcessor sendProcessor = new SendMessageProcessor(this);
    sendProcessor.registerSendMessageHook(sendMessageHookList);
    sendProcessor.registerConsumeMessageHook(consumeMessageHookList);

    this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);
    
    //......
    
    /**
     * PullMessageProcessor
     * 消费者客户端拉取消息处理器
     */
    this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE, this.pullMessageProcessor, this.pullMessageExecutor);
    this.pullMessageProcessor.registerConsumeMessageHook(consumeMessageHookList);  
}

处理器 SendMessageProcessor接受到生产者的发送请求后开始异步处理消息

public CompletableFuture<RemotingCommand> asyncProcessRequest(ChannelHandlerContext ctx,
                                                              RemotingCommand request) throws RemotingCommandException {
    final SendMessageContext mqtraceContext;
    switch (request.getCode()) {
        //TODO: 消费失败,发起重试,则会来到这里
        case RequestCode.CONSUMER_SEND_MSG_BACK:
            return this.asyncConsumerSendMsgBack(ctx, request);
        default:
            //TODO: 解析封装了消息体的 SendMessageRequestHeader
            SendMessageRequestHeader requestHeader = parseRequestHeader(request);
            if (requestHeader == null) {
                return CompletableFuture.completedFuture(null);
            }
            mqtraceContext = buildMsgContext(ctx, requestHeader);
            this.executeSendMessageHookBefore(ctx, request, mqtraceContext);
            if (requestHeader.isBatch()) {
                //TODO: 批量消息
                return this.asyncSendBatchMessage(ctx, request, mqtraceContext, requestHeader);
            } else {

                //TODO: 普通消息发送就会走这里
                return this.asyncSendMessage(ctx, request, mqtraceContext, requestHeader);
            }
    }
}

进入普通消息发送逻辑方法后,主要做2件事情

  1. 创建topic并持久化
//TODO: 预发送方法内部将会创建topic并持计划
final RemotingCommand response = preSend(ctx, request, requestHeader);
//....

//TODO: 调用内部的 msgCheck 方法
super.msgCheck(ctx, requestHeader, response);

//TODO: 创建topic信息并持久化
topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod(
    requestHeader.getTopic(),
    requestHeader.getDefaultTopic(),
    RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
    requestHeader.getDefaultTopicQueueNums(), topicSysFlag);

注意:只有当broker配置文件中指定 autoCreateTopicEnable=true 才可以创建topic并持久化,否则是无法创建的,只能通过控制台或者mqadmin命令去创建。默认就是true。
topic 持久化的默认存储路径是:$HOME/store/config/topics.json

  1. 构建服务端消息体对象MessageExtBrokerInner

设置topic, queueid, 消息内容body,以及TAGS等等(TAGS是设置到properties属性中),然后调用 DefaultMessageStore 对象的asyncPutMessage()方法put消息

putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);

继续调用CommitLog 对象的 asyncPutMessage()方法put消息

//TODO: 向commitlog写入消息数据
CompletableFuture<PutMessageResult> putResultFuture = this.commitLog.asyncPutMessage(msg);

在保存消息之前,CommitLog#asyncPutMessage()方法会主要做两件事情

  1. 判断消息是否为延迟消息
    1.1 如果不是延迟消息,则直接请看步骤2,否则请往下看
    1.2 将原始topic 替换为延迟消息专用的topic='SCHEDULE_TOPIC_XXXX'
    1.3 将原始queueid 替换为(延迟级别-1)
    1.4 备份原始topic和queueid, 知道消息体的 properties属性中,留着后面解析出来再次消费

这里不是本次讲解的重点,知道即可,后面在单独说明延迟消息,事务消息,批量消息等

  1. 获取最新的MappedFile并准备put消息
    2.1 什么是MappedFile ?

    它其实就是物理文件的映射,假如映射的是存储消息的文件,那么,文件大小固定1G, 文件名是20个固定长度的文件起始偏移量,文件顺序写入,写满了就往第二个文件里写。第一个文件名固定是 00000000000000000000(20个0),第二个固定是00000000001073741824(第二个文件的起始偏移量是1G),第三个文件名的偏移量是2G, 所以文件名都是固定的。

    为什么是 ‘假如’映射的是存储消息的文件?因为在代码逻辑设计上
    1个CommitLog 对应1个 MappedFileQueue 对应多个 MappedFile
    1个queueId 对应1个 MappedFileQueue 对应多个MappedFile
    所以 MappedFile 具有一定的通用性,它即可以与存储消息的commitlog映射,也可以与存储索引的queue映射 2.2 通过MappedFile 对象开始put消息

//TODO: 添加消息
result = mappedFile.appendMessage(msg, this.appendMessageCallback);

2.2 存储消息

put消息到缓冲区之前有几个参数说明一下

public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
    assert messageExt != null;
    assert cb != null;


    //TODO: 记录写入的位置,很有用,每次递增消息的totalSize
    int currentPos = this.wrotePosition.get();

    if (currentPos < this.fileSize) {
        ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
        byteBuffer.position(currentPos);
        AppendMessageResult result;
         //TODO: 普通消息,将走这里
        if (messageExt instanceof MessageExtBrokerInner) {
            //TODO: this.getFileFromOffset() 对应的是 fileFromOffset 属性,他就是每个mappedFile 文件的第一个偏移量,也就是文件名的值,Long.parseLong() 取得的,第一个文件的话就是0
            //TODO: 第三个参数maxBlank, 是文件大小(1G) -当前文件已经写入的数据
            result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
        } else if (messageExt instanceof MessageExtBatch) {  //TODO:批量消息
            result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
        } else {
            return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
        }


        //TODO: 写入成功后,记录写入的位置,这个很有用
        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);
}

wrotePositon 属性会记录每次写入的值

2.3 消息写入缓冲区

上面的代码中,这一行将会把消息写入缓冲区,cd.doAppend(...) 方法回调后会来到CommitLog内部中,进一步的逻辑处理

result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);

主要做了3件比较重要的事

2.3.1 计算消息索引

CommitLog 内部维护了一个Map,名为 topicQueueTable,其key 为 'topic-queueid', value 就是 offset. 那么要做的首先就是根据key获取offset, 第一次获取肯定是null, 那么就是置为0L.(当消息写入缓冲区成功后,offset自动加1,后面会说到,继续往下走)

如果broker重启,那么启动时会更新这个topicQueueTable

2.3.2 判断文件是否可以写得下当前消息

如果写到文件末尾写不下了,则新建一个MappedFile文件,重新写入消息

2.3.3 初始化存储空间,开始写入缓冲区

写入缓冲区的内容很多,共有17个之多,其中主要的有几个是消息的总大小,queueid, topic, 消息的物理偏移量PHYSICALOFFSET,consumequeue 的偏移量QUEUEOFFSET(QUEUEOFFSET就是2.3.1中维护的消息索引值,QUEUEOFFSET * 20byte 就可以定位在consumequeue中的实际位置)
写入缓冲区的内容

2.3.4 消息的索引offset自加1

当消息写入缓冲区后,topicQueueTable 的 value 值 offset++

后面消息分发构建索引单元的时候就会使用这个offset.

2.3.5 记录MappedFile 写入的位置

//TODO: 写入成功后,记录写入的位置,这个很有用
this.wrotePosition.addAndGet(result.getWroteBytes());

wrotePosition 会记录MappedFile已经写到了哪里

2.4 消息持久化

提交刷盘请求

  • 同步刷盘
  • 异步刷盘,异步刷盘的服务类是 FlushRealTimeService,每500ms执行一次刷盘任务

消息默认存储路径:$HOME/store/commitlog/{fileName}

3.消息分发(处理消息的索引条目)

当Broker(BrokerStartup)启动的时候会启动一个异步线程任务ReputMessageService,它会读取commitlog中的消息,进而创建索引单元
核心代码

class ReputMessageService extends ServiceThread {

    private volatile long reputFromOffset = 0;

    //......

    private void doReput() {
        if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
            log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
                this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
            this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
        }

        //TODO: this.isCommitLogAvailable() 就是判断commitlog已经写到了哪里
        for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {

            if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
                && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
                break;
            }

            //TODO: 1.从commitlog读取消息
            SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
            if (result != null) {
                try {
                    this.reputFromOffset = result.getStartOffset();

                    for (int readSize = 0; readSize < result.getSize() && doNext; ) {
                        DispatchRequest dispatchRequest =
                            DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
                        int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();

                        if (dispatchRequest.isSuccess()) {
                            if (size > 0) {
                                //TODO: 2.数据分发,核心逻辑
                                DefaultMessageStore.this.doDispatch(dispatchRequest);
                                //......
                                
                                //TODO: 3.数据分发offset 累加消息的size,下次从commitlog读取消息的起始offset
                                this.reputFromOffset += size;
                                //TODO: size就是消息的大小,每次分发完就+size,继续分发下一条消息
                                readSize += size;
                               
                               //......
                            } else if (size == 0) {
                                this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
                                readSize = result.getSize();
                            }
                        }
                    }
                } finally {
                    result.release();
                }
            } else {
                doNext = false;
            }
        }
    }

    @Override
    public void run() {
        DefaultMessageStore.log.info(this.getServiceName() + " service started");

        while (!this.isStopped()) {
            try {
                Thread.sleep(1);
                this.doReput();
            } catch (Exception e) {
                DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
            }
        }

        DefaultMessageStore.log.info(this.getServiceName() + " service end");
    }

    //......
}

3.1 从commitlog读取消息

//TODO:从commitlog读取消息
SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);

每次读取多少条消息呢?

前面有说到,当消息写入到缓冲区后,MappedFile对象的 wrotePosition属性会记录写入的位置,那么它从commitlog的读取消息的[start,end]偏移量就是 [reputFromOffset, wrotePostion.get()] 读取到消息后,开始遍历消息,然后分发

3.2 消息分发

//TODO: 消息分发的逻辑
DefaultMessageStore.this.doDispatch(dispatchRequest);

消息分发有两部分,一部分是针对消费者构建的consumequeue 索引,一个是根据消息key构建的索引

3.2.1 构建consumequeue 索引

消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件

3.2.1.1 分发过程

class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {

    //TODO: request 参数封装了消息体的全部信息
    @Override
    public void dispatch(DispatchRequest request) {
        final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
        switch (tranType) {
            case MessageSysFlag.TRANSACTION_NOT_TYPE:
            case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                //TODO: 构建消息索引
                DefaultMessageStore.this.putMessagePositionInfo(request);
                break;
            case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
            case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                break;
        }
    }
}
  1. 根据 topic 和 queueid 获取 ConsumeQueue 对象,ConsumeQueue 对象开始准备 put 索引数据

一个queueid 对应一个 ConsumeQueue, 一个ConsumeQueue 对应一个 MappedFileQueue, 一个 MappedFileQueue 对应多个 MappedFile

  1. 从分发对象request中读取出物理偏移量pyOffset,消息的大小size, 消息的Tag hashCode 按顺序写入缓冲区(这三个构成了一个消息的索引单元)
  2. 从分发对象request中读取出consumequeue的偏移量cqOffset, cq * 20(byte) 得到逻辑偏移量(因为每个consumequeue 索引单元固定20字节)expectLogicOffset

这个cqOffset 从哪里来?它就是2.3.1章节中的 topicQueueTable Map 维护的value值

  1. 更新ConsumeQueue 对象的最大物理偏移量maxPhysicOffset属性值
//TODO: offset 是物理的偏移量,size 就是当前这条消息的大小
this.maxPhysicOffset = offset + size;
  1. 根据 expectLogicOffset 获取最新的MappedFile文件对象(如果写满了就新建)
  2. 将写入缓冲区的索引单元数据追加到MappedFile中
  3. wrotePostion 记录索引单元写入的位置

3.2.1.2 持久化consumequeue

当Broker启动时会启动持久化服务 FlushConsumeQueueService

FlushConsumeQueueService 每隔1s执行一次持久化任务,写到对应topic下的queueid下的文件中 默认存储路径是:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}

  • 至此Consumequeue 索引构建完毕

3.2.2 构建 IndexFile 索引

IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法

class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {

    //TODO: request 参数封装了消息体的全部信息
    @Override
    public void dispatch(DispatchRequest request) {
        //TODO: messageIndexEnable 默认是true, 可以关闭IndexFile索引功能
        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
            DefaultMessageStore.this.indexService.buildIndex(request);
        }
    }
}

3.2.2.1 获取或者创建IndexFile文件

public IndexFile getAndCreateLastIndexFile() {
    IndexFile indexFile = null;
    IndexFile prevIndexFile = null;
    long lastUpdateEndPhyOffset = 0;
    long lastUpdateIndexTimestamp = 0;

    {
        this.readWriteLock.readLock().lock();
        if (!this.indexFileList.isEmpty()) {
            //TODO:获取最新的一个IndexFile文件
            IndexFile tmp = this.indexFileList.get(this.indexFileList.size() - 1);
            if (!tmp.isWriteFull()) {
                //TODO: 如果文件没有写满,则就使用这个最新的IndexFile
                indexFile = tmp;
            } else {
                lastUpdateEndPhyOffset = tmp.getEndPhyOffset();
                lastUpdateIndexTimestamp = tmp.getEndTimestamp();
                //TODO:如果写满了,则将写满了文件就标记为前一个
                prevIndexFile = tmp;
            }
        }

        this.readWriteLock.readLock().unlock();
    }

    //TODO: 需要新建IndexFile文件了
    if (indexFile == null) {
        try {
            String fileName =
                this.storePath + File.separator
                    + UtilAll.timeMillisToHumanString(System.currentTimeMillis());
            indexFile =
                new IndexFile(fileName, this.hashSlotNum, this.indexNum, lastUpdateEndPhyOffset,
                    lastUpdateIndexTimestamp);
            this.readWriteLock.writeLock().lock();
            //TODO: 将新建的IndexFile文件放入集合中
            this.indexFileList.add(indexFile);
        } catch (Exception e) {
            log.error("getLastIndexFile exception ", e);
        } finally {
            this.readWriteLock.writeLock().unlock();
        }

        //TODO: 新建了IndexFile或者原来的IndexFile还没有写满
        if (indexFile != null) {
            //TODO: 如果前一个IndexFile写满了,则执行刷盘
            final IndexFile flushThisFile = prevIndexFile;
            Thread flushThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    //TODO: 内部会对 flushThisFile 进行null判断,如果null则直接return
                    IndexService.this.flush(flushThisFile);
                }
            }, "FlushIndexFileThread");

            flushThread.setDaemon(true);
            flushThread.start();
        }
    }

    return indexFile;
}

从代码中不难发现,IndexFile的刷盘是只有当前一个IndexFile写满了才会刷盘。

3.2.2.2 检查是否需要创建索引

//TODO: uniqKey 是broker生成的,不为空,所以会根据uniqKey创建索引
if (req.getUniqKey() != null) {
    System.out.println("uniqKey: " + req.getUniqKey());
    indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
    if (indexFile == null) {
        log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
        return;
    }
}

//TODO: keys 是开发者指定的
if (keys != null && keys.length() > 0) {
    String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
    for (int i = 0; i < keyset.length; i++) {
        String key = keyset[i];
        if (key.length() > 0) {
            indexFile = putKey(indexFile, msg, buildKey(topic, key));
            if (indexFile == null) {
                log.error("putKey error commitlog {} uniqkey {}", req.getCommitLogOffset(), req.getUniqKey());
                return;
            }
        }
    }
}
  1. uniqKey 是broker生成的,不为空,当生产者发送完消息后返回的 SendResult 对象中有一个 msgId属性,他们是一样的

image.png

  1. keys 属性是生产者指定的
// 启动Producer实例
producer.start();
for (int i = 0; i < 1; i++) {
    // 创建消息,并指定Topic,Tag和消息体
    Message msg = new Message(MQConstants.DEBUG_TOPIC,
            "*",
            ("Hello RocketMQ, producer is qiuguan " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
    );

    //TODO: 指定keys
    msg.setKeys("keys-" + i);
    SendResult sendResult = producer.send(msg);
    // 通过sendResult返回消息是否成功送达
    System.out.printf("%s%s%n", sendResult, i);
}

keys 可以指定多个,可以用Collection集合参数,也可以用String参数,用String参数的话使用"空格"分割

image.png

3.2.2.3 保存索引

//TODO:buildKey()方法返回 topic + "#" + key
putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));

在put索引单元的时候有必要了解下IndexFile的构成

put的核心逻辑:

public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    if (this.indexHeader.getIndexCount() < this.indexNum) {
        //TODO: 对key取hash值
        int keyHash = indexKeyHashMethod(key);
        //TODO: 计算这个key 在 slot槽的位置(500w个)
        int slotPos = keyHash % this.hashSlotNum;

        //TODO: 计算slot的位置:固定40字节的IndexHeader + (slotPos * 每个slot槽固定4byte)
        int absSlotPos = IndexHeader.INDEX_HEADER_SIZE + slotPos * hashSlotSize;

        FileLock fileLock = null;

        try {

            // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
            // false);

            //TODO: 取出当前slot槽中存储的值 slotValue
            int slotValue = this.mappedByteBuffer.getInt(absSlotPos);
            if (slotValue <= invalidIndex || slotValue > this.indexHeader.getIndexCount()) {
                slotValue = invalidIndex;
            }

            //TODO: storeTimestamp 消息的存储时间(写到commitlog的时间)
            //TODO: this.indexHeader.getBeginTimestamp() 写入这个IndexFile第一条消息的时间
            long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();

            //TODO: timeDiff/1000, 这样占用4byte就够用了
            timeDiff = timeDiff / 1000;

            if (this.indexHeader.getBeginTimestamp() <= 0) {
                timeDiff = 0;
            } else if (timeDiff > Integer.MAX_VALUE) {
                timeDiff = Integer.MAX_VALUE;
            } else if (timeDiff < 0) {
                timeDiff = 0;
            }

            //TODO: 计算索引单元的位置
            //TODO: IndexHeader.INDEX_HEADER_SIZE 固定长度是40byte
            //TODO: this.hashSlotNum * hashSlotSize 就是500个slot槽 * 每个slot槽4byte
            //TODO: indexCount * 每个索引单元固定20byte (indexCount 在IndexHeader中有记录)
            int absIndexPos =
                IndexHeader.INDEX_HEADER_SIZE + this.hashSlotNum * hashSlotSize
                    + this.indexHeader.getIndexCount() * indexSize;

            //TODO: 按照顺序放入索引单元数据,
            //TODO: 分别是4byte的key的hash值,8byte的消息物理偏移量,4byte的(消息存储时间与当前IndexFile存储第一条消息的时间差)
            //TODO: 以及4byte上一次这个slot槽存储的值(它的存储结构类似于HashMap, 所以这个值就是用来解决hash冲突的)
            this.mappedByteBuffer.putInt(absIndexPos, keyHash);
            this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);

            //TODO: 这个slot槽存储的值indexCount的最大值
            this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());

            if (this.indexHeader.getIndexCount() <= 1) {
                //TODO: 写入IndexFile的第一条消息的物理偏移量放入IndexHeader中
                this.indexHeader.setBeginPhyOffset(phyOffset);
                //TODO: 写入IndexFile的第一条消息的时间放入IndexHeader中
                this.indexHeader.setBeginTimestamp(storeTimestamp);
            }

            if (invalidIndex == slotValue) {
                this.indexHeader.incHashSlotCount();
            }
            
            //TODO: indexCount++,统计在 IndexHeader中
            this.indexHeader.incIndexCount();
            //TODO: 将写入IndexFile的最后一条消息的物理偏移量写入IndexHeader中
            this.indexHeader.setEndPhyOffset(phyOffset);
            //TODO: 将写入IndexFile的最后一条消息的时间写入IndexHeader中
            this.indexHeader.setEndTimestamp(storeTimestamp);

            return true;
        } catch (Exception e) {
            log.error("putKey exception, Key: " + key + " KeyHashCode: " + key.hashCode(), e);
        } finally {
            if (fileLock != null) {
                try {
                    fileLock.release();
                } catch (IOException e) {
                    log.error("Failed to release the lock", e);
                }
            }
        }
    } else {
        log.warn("Over index file capacity: index count = " + this.indexHeader.getIndexCount()
            + "; index max num = " + this.indexNum);
    }

    return false;
}

参考我画的理解Index与slot的关系图
以及图解index写入过程

  • 至此Index索引构建完毕

3.3 更新消息分发偏移量 reputFromOffset

//TODO: 更新消息分发偏移量offset
this.reputFromOffset += size;
readSize += size;  //循环条件

消息分发偏移量 reputFromOffset 他是一直累加的,每分发一条消息,他就是累加消息的size,这样就可以持续从commitlog读取消息进而分发

4.小结

本文主要是从代码的角度看生产者如何发送消息,服务端broker如何接收消息存储消息持久化消息,以及消息分发(构建索引)。
ok, 就写到这里吧!

限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢