通过前面7.rocketmq源代码学习----服务端数据接收的学习,我们知道当客户端发送消息时,服务端的处理器是:PullMessageProcessor,接下来我们就来分析下PullMessageProcessor的代码。
从前面文章的分析,我们知道拉取文件肯定是需要从consumequeue中获取消息id,先来简要看下PullMessageProcessor的简单时序图:
有几个关键的问题:
1、是怎么根据offset找到文件的?
2、是怎么根据offset找到对应位置的?
3、tag过滤存在hash冲突的,hash一致不代表tag一致,rocktemq是怎么处理的?
先来看第1个第2个问题:
1、是怎么根据offset找到文件的?
2、是怎么根据offset找到对应位置的?
根据前面消息存储结构的分析:https://blog.csdn.net/u013286936/article/details/85308290
我们知道consumequeue文件里面,一个存储单元固定是20byte,那么一个consumequeue文件存储多少个单元呢?
在MessageStoreConfig中可以找到答案:
// ConsumeQueue每个文件大小 默认存储30W条消息
private int mapedFileSizeConsumeQueue = 300000 * ConsumeQueue.CQStoreUnitSize;
也就是一个consumequeue文件存储30w个单元,每个单元20byte,也就是一个文件是:30w*20byte = 600wbyte
consumequeu的文件命名规则是:(n-1) * 600w,n是第几个文件。
即文件列表将是:
- 00000000000000000000
- 00000000000006000000
- 00000000000012000000
- 00000000000018000000
- 00000000000024000000
- 00000000000030000000
- (n-1) * 600w…
好了,有了以上条件后,如果拉取queue的offset是 600100
那么根据:每个存储单元占据20byte,则对应的文件索引是:60010020byte / (30w20byte)(每个文件大小),即第
3个文件,由于rocketmq有清理机制,即 第一个文件可能不存在,所以需要减掉已经清理的文件。来看下代码:
ConsumeQueue.getIndexBuffer()
/**
* 返回Index Buffer
*
* @param startIndex
* 起始偏移量索引
*/
public SelectMapedBufferResult getIndexBuffer(final long startIndex) {
int mapedFileSize = this.mapedFileSize;
long offset = startIndex * CQStoreUnitSize;
if (offset >= this.getMinLogicOffset()) {
MapedFile mapedFile = this.mapedFileQueue.findMapedFileByOffset(offset);
if (mapedFile != null) {
SelectMapedBufferResult result = mapedFile.selectMapedBuffer((int) (offset % mapedFileSize));
return result;
}
}
return null;
}
MapedFileQueue.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;
}
再来看第3个问题:
当消费者带tag订阅时,服务端只判断了hash是否一致,那当hash冲突的是在哪里处理的,来看下客户端代码:
DefaultMqPushConsumerImpl.pullMessage
PullCallback pullCallback = new PullCallback() {
@Override
public void onSuccess(PullResult pullResult) {
if (pullResult != null) {
pullResult =
DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(
pullRequest.getMessageQueue(), pullResult, subscriptionData);
switch (pullResult.getPullStatus()) {
pullAPIWrapper.processPullResult
public PullResult processPullResult(final MessageQueue mq, final PullResult pullResult,
final SubscriptionData subscriptionData) {
final String projectGroupPrefix = this.mQClientFactory.getMQClientAPIImpl().getProjectGroupPrefix();
PullResultExt pullResultExt = (PullResultExt) pullResult;
this.updatePullFromWhichNode(mq, pullResultExt.getSuggestWhichBrokerId());
if (PullStatus.FOUND == pullResult.getPullStatus()) {
ByteBuffer byteBuffer = ByteBuffer.wrap(pullResultExt.getMessageBinary());
List<MessageExt> msgList = MessageDecoder.decodes(byteBuffer);
List<MessageExt> msgListFilterAgain = msgList;
if (!subscriptionData.getTagsSet().isEmpty() && !subscriptionData.isClassFilterMode()) {
msgListFilterAgain = new ArrayList<MessageExt>(msgList.size());
for (MessageExt msg : msgList) {
if (msg.getTags() != null) {
if (subscriptionData.getTagsSet().contains(msg.getTags())) {
msgListFilterAgain.add(msg);
}
}
}
}
if (this.hasHook()) {
FilterMessageContext filterMessageContext = new FilterMessageContext();
filterMessageContext.setUnitMode(unitMode);
filterMessageContext.setMsgList(msgListFilterAgain);
this.executeHook(filterMessageContext);
}
if (!UtilAll.isBlank(projectGroupPrefix)) {
subscriptionData.setTopic(VirtualEnvUtil.clearProjectGroup(subscriptionData.getTopic(),
projectGroupPrefix));
mq.setTopic(VirtualEnvUtil.clearProjectGroup(mq.getTopic(), projectGroupPrefix));
for (MessageExt msg : msgListFilterAgain) {
msg.setTopic(VirtualEnvUtil.clearProjectGroup(msg.getTopic(), projectGroupPrefix));
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MIN_OFFSET,
Long.toString(pullResult.getMinOffset()));
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MAX_OFFSET,
Long.toString(pullResult.getMaxOffset()));
}
}
else {
for (MessageExt msg : msgListFilterAgain) {
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MIN_OFFSET,
Long.toString(pullResult.getMinOffset()));
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_MAX_OFFSET,
Long.toString(pullResult.getMaxOffset()));
}
}
pullResultExt.setMsgFoundList(msgListFilterAgain);
}
pullResultExt.setMessageBinary(null);
return pullResult;
}
也就是在客户端再做了一层过滤,比对了tag真正的值
为什么要这么做呢?
tag 在服务端以hashcode的形式存储,这样是定长的,可以提高性能,为什么呢?看上面拉取消息的代码可以知道,根据offset能很迅速的定位到消息在哪一个文件、哪个位置,无需遍历