1 getMessage方法
上一篇文章中根据Broker “拉消息”请求处理的源码梳理出原理并进行学习,本篇文章中将对相关源码进行进一步探究。
在PullMessageProcessor中,会调用DefaultMessageStore类中的getMessage方法,源码梳理如下:
/**
* @param group 消费者组
* @param topic 主题
* @param queueId
* @param offset 客户端拉消息使用位点
* @param maxMsgNums 32
* @param messageFilter 一般这里是 tagCode 过滤 在服务器这里
*/
public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset,
final int maxMsgNums,
final MessageFilter messageFilter) {
if (this.shutdown) {
log.warn("message store has shutdown, so getMessage is forbidden");
return null;
}
if (!this.runningFlags.isReadable()) {
log.warn("message store is not readable, so getMessage is forbidden " + this.runningFlags.getFlagBits());
return null;
}
// 查询开始时间
long beginTime = this.getSystemClock().now();
// status 查询结果状态,默认值:NO_MESSAGE_IN_QUEUE 后面马上会改!
GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
// nextBeginOffset 客户端下一次 pull 时使用的 位点信息,默认值:pullRequest.offset 后面拉取到消息之后,也会修改该值
long nextBeginOffset = offset;
// 表示queue的最小offset 和 最大 offset (注意:这里的单位 为 “CQData”)
long minOffset = 0;
long maxOffset = 0;
// 查询结果对象
GetMessageResult getResult = new GetMessageResult();
// 获取commitLog 最大物理偏移量:当前正在顺序写的mf文件 文件名 long 值 + 顺序写文件的 position
final long maxOffsetPy = this.commitLog.getMaxOffset();
// 看源码得知,该方法不会返回null,会拿到指定主题指定queueid的ConsumeQueue对象
// (它是管理队列文件,存的是CQData(1.offsetPy 2. size 3.tagCode))
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
if (consumeQueue != null) {
// 正常情况执行这里:
// 表示queue的最小offset 和 最大 offset (注意:这里的单位 为 “CQData”)
minOffset = consumeQueue.getMinOffsetInQueue();
maxOffset = consumeQueue.getMaxOffsetInQueue();
if (maxOffset == 0) {
// 一定是 查询时调用 findConsumeQueue(..)导致创建了 该 ConsumeQueue 时(本来不存在),它的maxOffset 才会是0
// 队列内无数据,设置状态为 NO_MESSAGE_IN_QUEUE 外层“PullMessageProcess”会进行长轮询的
status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
// 调整offset 为0
nextBeginOffset = nextOffsetCorrection(offset, 0);
} else if (offset < minOffset) {
// 设置状态“OFFSET_TOO_SMALL”...
status = GetMessageStatus.OFFSET_TOO_SMALL;
// 调整offset 为 minOffset
nextBeginOffset = nextOffsetCorrection(offset, minOffset);
} else if (offset == maxOffset) {
// 说明客户端消费进度 和 该queue的消息进度 持平了...
// PullMessageProcess 会进入长轮询的逻辑..
status = GetMessageStatus.OFFSET_OVERFLOW_ONE;
// offset 不发生变化
nextBeginOffset = nextOffsetCorrection(offset, offset);
} else if (offset > maxOffset) {
// 说明客户端请求使用的offset 一定是一个有问题的 offset
// 设置状态为“OFFSET_OVERFLOW_BADLY”
status = GetMessageStatus.OFFSET_OVERFLOW_BADLY;
// 根据 minOffset 是否为0,调整 nextBeginOffset....
if (0 == minOffset) {
nextBeginOffset = nextOffsetCorrection(offset, minOffset);
} else {
nextBeginOffset = nextOffsetCorrection(offset, maxOffset);
}
} else {
// 执行到该 else 代码块 说明 pullRequest.offset 是满足ConsumeQueue有效数据范围的offset
// 根据 offset 查询 CQData数据,CQData数据由 smbr 表示,smbr 内部有 byteBuffer
// bufferConsumeQueue数据范围:
// 1. 如果offset 命中的文件不是 正在顺序写的文件的话 [offset表示的这条消息,文件尾]
// 2. 如果offset 命中的文件是 正在顺序写的文件的话 [offset表示的这条消息,文件名+wrotePosition]
SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset);
// 只有很极端的时候 才会 bufferConsumeQueue == null . 刚刚赶上 consumeQueue 删除过期数据的逻辑执行.
//删除逻辑:先删除CommitLog文件,删除之后CommitLog最小的物理offset调用ConsumerQueue的删除数据文件的方法
if (bufferConsumeQueue != null) {
try {
// 初始状态为 未匹配到消息
status = GetMessageStatus.NO_MATCHED_MESSAGE;
// 下一个commitLog物理文件名(初始值是 long 最小值..)
long nextPhyFileStartOffset = Long.MIN_VALUE;
// 本次拉消息 最后一条消息 它的 物理偏移量
long maxPhyOffsetPulling = 0;
int i = 0;
// 16000
final int maxFilterMessageCount = Math.max(16000, maxMsgNums * ConsumeQueue.CQ_STORE_UNIT_SIZE);
final boolean diskFallRecorded = this.messageStoreConfig.isDiskFallRecorded();
ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
for (; i < bufferConsumeQueue.getSize() && i < maxFilterMessageCount; i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
// 从bufferConsumeQueue的byteBuffer中 读取 20 个字节
// 消息物理偏移量
long offsetPy = bufferConsumeQueue.getByteBuffer().getLong();
// 消息大小
int sizePy = bufferConsumeQueue.getByteBuffer().getInt();
// 消息 tagCode
long tagsCode = bufferConsumeQueue.getByteBuffer().getLong();
// 本次拉消息 最后一条消息 它的 物理偏移量
maxPhyOffsetPulling = offsetPy;
if (nextPhyFileStartOffset != Long.MIN_VALUE) {
if (offsetPy < nextPhyFileStartOffset)
continue;
}
// 参数1:offsetPy,本次循环处理的CQData代表的消息 它的物理偏移量
// 参数2:maxOffsetPy,当前broker节点 commitLog 内最大的物理偏移量
// 返回值:boolean false 表示offsetPy这条消息为 “热数据” 否则 为 “冷数据”
boolean isInDisk = checkInDiskByCommitOffset(offsetPy, maxOffsetPy);
// 控制是否跳出循环
// 参数1:sizePy,本次循环CQData表示消息大小
// 参数2:maxMsgNums 32
// 参数3:getResult.getBufferTotalSize() 本次查询 已经获取的消息总size
// 参数4:getResult.getMessageCount() 本次查询 已经获取的消息个数
// 参数5:本轮循环处理的CQData表示的msg,是否为热数据 false 表示offsetPy这条消息为 “热数据” 否则 为 “冷数据”
if (this.isTheBatchFull(sizePy, maxMsgNums, getResult.getBufferTotalSize(), getResult.getMessageCount(),
isInDisk)) {
break;
}
boolean extRet = false, isTagsCodeLegal = true;
if (consumeQueue.isExtAddr(tagsCode)) {
extRet = consumeQueue.getExt(tagsCode, cqExtUnit);
if (extRet) {
tagsCode = cqExtUnit.getTagsCode();
} else {
// can't find ext content.Client will filter messages by tag also.
log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}, topic={}, group={}",
tagsCode, offsetPy, sizePy, topic, group);
isTagsCodeLegal = false;
}
}
// 服务器端按照 消息 tagCode 进行过滤
if (messageFilter != null
&& !messageFilter.isMatchedByConsumeQueue(isTagsCodeLegal ? tagsCode : null, extRet ? cqExtUnit : null)) {
if (getResult.getBufferTotalSize() == 0) {
status = GetMessageStatus.NO_MATCHED_MESSAGE;
}
continue;
}
// 根据 CQData.offsetPy 和 CQData.sizePy 到 commitLog 查询出这条msg,msg由 smbr表示
SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);
// 什么时候 selectResult == null ? 查询之前 commitLog 删除过期文件的定时任务 刚刚好执行过,将包含offsetPy
// 的数据文件 删除掉了..
if (null == selectResult) {
if (getResult.getBufferTotalSize() == 0) {
status = GetMessageStatus.MESSAGE_WAS_REMOVING;
}
// 获取包含该“offsetPy”数据文件的下一个数据文件的文件名(long)
nextPhyFileStartOffset = this.commitLog.rollNextFile(offsetPy);
continue;
}
if (messageFilter != null
&& !messageFilter.isMatchedByCommitLog(selectResult.getByteBuffer().slice(), null)) {
if (getResult.getBufferTotalSize() == 0) {
status = GetMessageStatus.NO_MATCHED_MESSAGE;
}
// release...
selectResult.release();
continue;
}
this.storeStatsService.getGetMessageTransferedMsgCount().incrementAndGet();
// 将本次循环查询出来的 msg smbr 加入到 getResult 内
getResult.addMessage(selectResult);
// 查询状态设置为 “FOUND”
status = GetMessageStatus.FOUND;
// 强制设置 nextPhyFileStartOffset 为 long 最小值,避免走上面 跳过期CQData 数据的逻辑。
nextPhyFileStartOffset = Long.MIN_VALUE;
}
if (diskFallRecorded) {
long fallBehind = maxOffsetPy - maxPhyOffsetPulling;
brokerStatsManager.recordDiskFallBehindSize(group, topic, queueId, fallBehind);
}
// nextBeginOffset 客户端下一次 pull 时使用的 位点信息,默认值:pullRequest.offset 后面拉取到消息之后,也会修改该值
// 怎么修改? pull.offset + 上面for循环读取过的CQdata字节数 / 20
nextBeginOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
// diff=commitLog最大物理偏移量 - 本次拉消息最后一条消息的物理偏移量
long diff = maxOffsetPy - maxPhyOffsetPulling;
// 40% 系统内存
long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE
* (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0));
// diff > memory => true 表示本轮查询 最后一条消息 为 冷数据,broker服务器建议客户端下一次 pull时到 slave节点
// diff > memory => false 表示本轮查询 最后一条消息 为 热数据,broker服务器建议客户端下一次 pull时到 master节点
getResult.setSuggestPullingFromSlave(diff > memory);
} finally {
// 释放consumeQueue查询时返回的 smbr 对象
bufferConsumeQueue.release();
}
} else {
status = GetMessageStatus.OFFSET_FOUND_NULL;
nextBeginOffset = nextOffsetCorrection(offset, consumeQueue.rollNextFile(offset));
log.warn("consumer request topic: " + topic + "offset: " + offset + " minOffset: " + minOffset + " maxOffset: "
+ maxOffset + ", but access logic queue failed.");
}
}
} else {
status = GetMessageStatus.NO_MATCHED_LOGIC_QUEUE;
nextBeginOffset = nextOffsetCorrection(offset, 0);
}
if (GetMessageStatus.FOUND == status) {
this.storeStatsService.getGetMessageTimesTotalFound().incrementAndGet();
} else {
this.storeStatsService.getGetMessageTimesTotalMiss().incrementAndGet();
}
long elapsedTime = this.getSystemClock().now() - beginTime;
this.storeStatsService.setGetMessageEntireTimeMax(elapsedTime);
// 设置结果状态
getResult.setStatus(status);
// 设置客户端下一次pull时的offset
getResult.setNextBeginOffset(nextBeginOffset);
// 设置queue的最大offset 和 最小 offset
getResult.setMaxOffset(maxOffset);
getResult.setMinOffset(minOffset);
return getResult;
}
1.1 checkInDiskByCommitOffset方法
该方法获取这条消息为 “热数据” 否则 为 “冷数据”
// 参数1:offsetPy,本次循环处理的CQData代表的消息 它的物理偏移量
// 参数2:maxOffsetPy,当前broker节点 commitLog 内最大的物理偏移量
// 返回值:boolean false 表示offsetPy这条消息为 “热数据” 否则 为 “冷数据”
private boolean checkInDiskByCommitOffset(long offsetPy, long maxOffsetPy) {
// memory 系统40%内存的字节数
long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0));
return (maxOffsetPy - offsetPy) > memory;
}
1.2 isTheBatchFull方法
该方法判断是否跳出循环
// 参数1:sizePy,本次循环CQData表示消息大小
// 参数2:maxMsgNums 32
// 参数3:getResult.getBufferTotalSize() 本次查询 已经获取的消息总size
// 参数4:getResult.getMessageCount() 本次查询 已经获取的消息个数
// 参数5:本轮循环处理的CQData表示的msg,是否为热数据 false 表示offsetPy这条消息为 “热数据” 否则 为 “冷数据”
// 返回值:true 外层跳出循环 false 外层继续
private boolean isTheBatchFull(int sizePy, int maxMsgNums, int bufferTotal, int messageTotal, boolean isInDisk) {
// 条件成立:说明本次pull消息,还未拉取到任何东西..需要外层for循环继续,返回false
if (0 == bufferTotal || 0 == messageTotal) {
return false;
}
// 条件成立:说明结果对象内 消息数 已经 够量了(maxMsgNums)大于等于32了
if (maxMsgNums <= messageTotal) {
return true;
}
if (isInDisk) {
// CASE 冷数据
// 冷数据 pull 查询最多一次可以 pull 64kb 消息
if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInDisk()) {
return true;
}
// 冷数据 pull 查询最多一次可以 pull 8 条消息
if (messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInDisk() - 1) {
return true;
}
} else {
// CASE 热数据
// 热数据 pull 查询最多一次可以 pull 256kb 消息
if ((bufferTotal + sizePy) > this.messageStoreConfig.getMaxTransferBytesOnMessageInMemory()) {
return true;
}
// 热数据 pull 查询最多一次可以 pull 32 条消息
if (messageTotal > this.messageStoreConfig.getMaxTransferCountOnMessageInMemory() - 1) {
return true;
}
}
return false;
}