1. 前言
在RocketMQ体系中,系统预定义了一系列的Topic,开发者自定义的Topic名称不可以和它们重名,否则会抛异常。这些预定义的系统Topic都有它们各自的用途,例如RMQ_SYS_TRANS_OP_HALF_TOPIC
用来存放半事务消息,SCHEDULE_TOPIC_XXXX
用来存放延时消息等等。其中有一个Topic较为特殊,它就是TBW102
,它有什么用呢?
Producer在发送消息时,默认情况下,不需要提前创建好Topic,如果Topic不存在,Broker会自动创建Topic。但是新创建的Topic它的权限是什么?读写队列数是多少呢?这个时候就需要用到TBW102
了,RocketMQ会基于该Topic的配置创建新的Topic。
Broker通过属性autoCreateTopicEnable
来设置是否开启自动创建Topic,如果配置为true,但是删除了TBW102,此时Producer发送一个不存在的Topic消息,Broker就会抛异常,这点需要注意。
2. 源码分析
2.1 Producer处理
Producer发送消息时,首先需要从NameServer拉取Topic路由信息,对应的方法是tryToFindTopicPublishInfo
。因为是新Topic,NameServer必然不存在路由数据,所以第一次会拉取失败。不过没关系,Producer会进行第二次拉取。
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
// 先从本地缓存获取
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
// 没有缓存,或失效
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
// 发请求从NameServer拉取,并更新缓存
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
// 不存在的Topic,第一次拉取不到,第二次通过TBW102拉取
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
第二次拉取时isDefault
设为true,这表示它会拉取TBW102的路由数据,只要你没有人为的删除,那么它必然可以拉取成功。Producer会基于TBW102的配置构建TopicPublishInfo并缓存到本地,然后进行正常的消息发送。注意:此时Topic在Broker中仍然不存在!
TopicRouteData topicRouteData;
if (isDefault && defaultMQProducer != null) {
// 不存在的Topic,第二次通过TBW102拉取
topicRouteData = this.mQClientAPIImpl
.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),1000 * 3);
if (topicRouteData != null) {
for (QueueData data : topicRouteData.getQueueDatas()) {
int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
data.setReadQueueNums(queueNums);
data.setWriteQueueNums(queueNums);
}
}
}
TopicRouteData设置好了,再调用topicRouteData2TopicPublishInfo()
将其转换为TopicPublishInfo对象。因为是消息发送,所以Producer会根据writeQueueNums
写队列数来创建对应数量的MessageQueue。
Producer虽然基于TBW102的配置创建了新的Topic路由数据,但是在Broker上,该Topic并不存在。Producer发送消息时,会在Header里的defaultTopic
属性带上TBW102,这样Broker发现消息Topic不存在时,会基于它自动创建新的Topic。
2.2 Broker处理
消息被正常发送到Broker后,会被SendMessageProcessor类处理。存储消息前,会先对消息做校验,方法是msgCheck()
。
消息校验时,一旦发现Topic不存在,会通过TopicConfigManager自动创建新的Topic。
// 从Map中取出Topic配置
TopicConfig topicConfig =
this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
if (null == topicConfig) {
// Topic不存在
int topicSysFlag = 0;
// 自动创建Topic
topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod(
requestHeader.getTopic(),
requestHeader.getDefaultTopic(),
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.getDefaultTopicQueueNums(), topicSysFlag);
if (null == topicConfig) {
// Topic创建失败
response.setCode(ResponseCode.TOPIC_NOT_EXIST);
response.setRemark("topic[" + requestHeader.getTopic() + "] not exist, apply first please!"
+ FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
return response;
}
}
Broker通过TopicConfigManager类管理Topic数据,存储在ConcurrentMap中,Broker这个时候会基于请求头里的defaultTopic
去创建新的Topic,创建成功后会写入ConcurrentMap,然后发心跳给NameServer,将Topic注册上去,这样其它Producer就能感知到新的Topic了,方法是createTopicInSendMessageMethod()
。
topicConfig = this.topicConfigTable.get(topic);
if (topicConfig != null)
// 已经存在,直接返回
return topicConfig;
// 获取TBW102配置
TopicConfig defaultTopicConfig = this.topicConfigTable.get(defaultTopic);
if (defaultTopicConfig != null) {
if (defaultTopic.equals(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC)) {
if (!this.brokerController.getBrokerConfig().isAutoCreateTopicEnable()) {
defaultTopicConfig.setPerm(PermName.PERM_READ | PermName.PERM_WRITE);
}
}
// 基于TBW102创建新的TopicConfig
if (PermName.isInherited(defaultTopicConfig.getPerm())) {
topicConfig = new TopicConfig(topic);
int queueNums = Math.min(clientDefaultTopicQueueNums, defaultTopicConfig.getWriteQueueNums());
if (queueNums < 0) {
queueNums = 0;
}
// 读写队列数
topicConfig.setReadQueueNums(queueNums);
topicConfig.setWriteQueueNums(queueNums);
// 权限
int perm = defaultTopicConfig.getPerm();
perm &= ~PermName.PERM_INHERIT;
topicConfig.setPerm(perm);
topicConfig.setTopicSysFlag(topicSysFlag);
topicConfig.setTopicFilterType(defaultTopicConfig.getTopicFilterType());
} else {
log.warn("Create new topic failed, because the default topic[{}] has no perm [{}] producer:[{}]",
defaultTopic, defaultTopicConfig.getPerm(), remoteAddress);
}
} else {
log.warn("Create new topic failed, because the default topic[{}] not exist. producer:[{}]",
defaultTopic, remoteAddress);
}
if (topicConfig != null) {
log.info("Create new topic by default topic:[{}] config:[{}] producer:[{}]",
defaultTopic, topicConfig, remoteAddress);
// 写入本地缓存,发心跳给NameServer时就会注册新的Topic了
this.topicConfigTable.put(topic, topicConfig);
this.dataVersion.nextVersion();
createNew = true;
// 持久化到磁盘
this.persist();
}
如果新的Topic创建成功,需要将其马上注册到NameServer中,这样其它Producer就能感知到了。
if (createNew) {
// 创建了新的Topic,注册到Broker上
this.brokerController.registerBrokerAll(false, true, true);
}
至此,一个全新的Topic被创建,后续就是正常的消息写入CommitLog的流程了。
3. 总结
TBW102的作用是,当Producer发送一个不存在的Topic消息时,Broker可以基于它的配置自动创建新的Topic,新创建的Topic权限、读写队列数等均继承自TBW102,因此它的配置极为重要。不过一般情况下,还是推荐提前创建好Topic。
整个流程总结一下,Producer发送一个不存在的Topic消息时,首先会从NameServer拉取Topic路由数据,第一次拉取必然失败,第二次会直接拉取TBW102的路由数据,基于它创建TopicPublishInfo并缓存到本地,进行正常的消息发送,在Header里将defaultTopic
设置为TBW102。Broker接收到消息时,先对消息做Check,检查到Topic不存在,会基于defaultTopic
的配置去创建该Topic,然后注册到NameServer上,这样一个全新的Topic就被自动创建了。