本文已参与[新人创作礼]活动,一路开启掘金创作之路。
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();
}
}
```
发送消息的大致流程如下:
- 根据topic从nameServer获取路由表信息
- 选择一个MessageQueue(在broker模式下手动创建默认是4个)
2.1
轮询算法(默认)
2.2
最小投递延迟算法 - 构建消息体 SendMessageRequestHeader对象(设置topic, queueid, 以及将TAGS设置到properties中等等)
- 构建远程netty请求命令(这里关注一下,后面服务端接收消息就是根据这个)
RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader)
- 发送消息
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件事情
- 创建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
- 构建服务端消息体对象
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
如果不是延迟消息,则直接请看步骤2,否则请往下看
1.2
将原始topic 替换为延迟消息专用的topic='SCHEDULE_TOPIC_XXXX'
1.3
将原始queueid 替换为(延迟级别-1)
1.4
备份原始topic和queueid, 知道消息体的 properties属性中,留着后面解析出来再次消费
这里不是本次讲解的重点,知道即可,后面在单独说明延迟消息,事务消息,批量消息等
-
获取最新的
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;
}
}
}
- 根据 topic 和 queueid 获取
ConsumeQueue
对象,ConsumeQueue
对象开始准备 put 索引数据
一个queueid 对应一个 ConsumeQueue, 一个ConsumeQueue 对应一个 MappedFileQueue, 一个 MappedFileQueue 对应多个 MappedFile
- 从分发对象request中读取出物理偏移量pyOffset,消息的大小size, 消息的Tag hashCode 按顺序写入缓冲区(这三个构成了一个消息的索引单元)
- 从分发对象request中读取出consumequeue的偏移量cqOffset, cq * 20(byte) 得到逻辑偏移量(因为每个consumequeue 索引单元固定20字节)
expectLogicOffset
这个cqOffset 从哪里来?它就是2.3.1章节中的
topicQueueTable
Map 维护的value值
- 更新
ConsumeQueue
对象的最大物理偏移量maxPhysicOffset
属性值
//TODO: offset 是物理的偏移量,size 就是当前这条消息的大小
this.maxPhysicOffset = offset + size;
- 根据
expectLogicOffset
获取最新的MappedFile文件对象(如果写满了就新建) - 将写入缓冲区的索引单元数据追加到MappedFile中
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;
}
}
}
}
uniqKey
是broker生成的,不为空,当生产者发送完消息后返回的SendResult
对象中有一个msgId
属性,他们是一样的
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
参数的话使用"空格"分割
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, 就写到这里吧!
限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢