RocketMQ Topic是如何注册和保存的

Topic

Topic用于标识一些消息的分类,例如订单消息,通知消息。RocketMQ Producer发送消息,Consumer接收消息,Topic都是绕不过去的话题,消息就是围绕Topic组织的。Topic存储在NameSrv,Producer从NameSrv获取Topic的路由信息,找到broker,然后发送消息至broker。Consumer同样从NameSrv获取Topic路由信息,找到broker,然后从broker拉取消息,进行消费。
在这里插入图片描述
既然所有的topic路由信息是存在NameSrv端的,那么创建Topic,是不是就直接向NameSrv发送请求呢?

因为客户端只随机与NameSrv的其中一个建立长连接(很好理解吧?),如果直接在NameSrv注册路由信息的话,那么NameSrv势必需要与其它的NameSrv保持通讯,这样才能使得所有的结点的路由信息保持完整和一致。但官方文档上面明确表示,NameSrv是无状态的,结点之间无任何信息同步,如下图所示:
在这里插入图片描述
事情似乎没有想的那么简单。。。

从创建Topic命令说起

rocketmq bin目录下有mqadmin工具,创建topic命令如下所示:

./mqadmin updateTopic -n 192.168.77.129:9876 -c DefaultCluster -t TestTopic

这个命令很有迷惑性,通过-n参数指定了NameSrv地址,很容易以为真的就是直接向NameSrv注册Topic路由信息。
mqadmin工具的实现是在rocketmq-tools工程实现的,具体到updateTopic这个命令是UpdateTopicSubCommand这个类实现的。
在解析-c参数的时候,拿到broker集群名称,然后通过CommandUtil.fetchMasterAddrByClusterName() 获取broker集群下所有master的brokerName。然后调用defaultMQAdminExt.createAndUpdateTopicConfig() 向集群中的所有master结点注册topic。

else if (commandLine.hasOption('c')) {
                String clusterName = commandLine.getOptionValue('c').trim();

                defaultMQAdminExt.start();

                Set<String> masterSet =
                    CommandUtil.fetchMasterAddrByClusterName(defaultMQAdminExt, clusterName);
                for (String addr : masterSet) {
                    defaultMQAdminExt.createAndUpdateTopicConfig(addr, topicConfig);
                    System.out.printf("create topic to %s success.%n", addr);
                }

                // 省略无关代码

                System.out.printf("%s", topicConfig);
                return;
            }

TopicConfig

客户端向Broker提交的Topic信息封装在了TopicConfig里面,TopicConfig类如下所示:

public class TopicConfig {
    private static final String SEPARATOR = " ";
    public static int defaultReadQueueNums = 16; // 默认读队列数
    public static int defaultWriteQueueNums = 16; // 默认写队列数
    private String topicName; // topic名称
    private int readQueueNums = defaultReadQueueNums;
    private int writeQueueNums = defaultWriteQueueNums;
    private int perm = PermName.PERM_READ | PermName.PERM_WRITE; // 操作权限,这里初始化是可读可写
    private TopicFilterType topicFilterType = TopicFilterType.SINGLE_TAG; // topic过滤类型,默认单tag
    private int topicSysFlag = 0; // 
    private boolean order = false; // 

    public TopicConfig() {
    }
)

TopicConfig保存了Topic名称,读写队列数,读写权限等信息。

Broker拿到Topic路由信息后干啥了

RocketMQ的通讯是基于Netty的,对其进行了简单的封装,所有的通讯操作都是根据RquestCode来区分的,后面会单独讲mq的通讯。这里创建Topic的RequestCode是:

public static final int UPDATE_AND_CREATE_TOPIC = 17;

客户端的创建Topic的调用在这里:
在这里插入图片描述
根据UPDATE_AND_CREATE_TOPIC搜索代码,我们可以在broker工程里找到处理Topic请求的地方:
在这里插入图片描述
updateAndCreateTopic方法主要做了两件事,1-更新本地的topic配置,2-向所有的NameSrv注册broker信息。如下所示:

 this.brokerController.getTopicConfigManager().updateTopicConfig(topicConfig);
 this.brokerController.registerBrokerAll(false, true);

Topic信息在Broker端的保存

TopicConfig在broker端是保存在TopicConfigManager的,如下所示:

public class TopicConfigManager extends ConfigManager {
    private static final Logger LOG = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME);
    private static final long LOCK_TIMEOUT_MILLIS = 3000;
    private transient final Lock lockTopicConfigTable = new ReentrantLock();

    private final ConcurrentMap<String, TopicConfig> topicConfigTable =
        new ConcurrentHashMap<String, TopicConfig>(1024); // topic信息保存在这里
    private final DataVersion dataVersion = new DataVersion();
    private final Set<String> systemTopicList = new HashSet<String>();
    private transient BrokerController brokerController;

    public TopicConfigManager() {
    }

更新完内存中的topic信息后,broker还会以json格式将其持久化到磁盘上。

public void updateTopicConfig(final TopicConfig topicConfig) {
        TopicConfig old = this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
        if (old != null) {
            LOG.info("update topic config, old:[{}] new:[{}]", old, topicConfig);
        } else {
            LOG.info("create new topic [{}]", topicConfig);
        }

        this.dataVersion.nextVersion();

        this.persist();
    }

我们进入到store/config目录下,可以看到topics.json文件:
在这里插入图片描述
打开topics.json文件后,内容如下:
在这里插入图片描述

注册Broker信息

之前讲到broker拿到topic信息后,做了两件事情:将Toppic保存到本地,然后向NameSrv注册broker信息。broker信息就是在topic信息之上附加了broker的相关信息,例如:集群名称、broker名称、ip地址、broker id等。注册代码如下:

 RegisterBrokerResult registerBrokerResult = this.brokerOuterAPI.registerBrokerAll(
            this.brokerConfig.getBrokerClusterName(),
            this.getBrokerAddr(),
            this.brokerConfig.getBrokerName(),
            this.brokerConfig.getBrokerId(),
            this.getHAServerAddr(),
            topicConfigWrapper,
            this.filterServerManager.buildNewFilterServerList(),
            oneway,
            this.brokerConfig.getRegisterBrokerTimeoutMills());

broker master会向所有的NameSrv结点发起注册请求,请求命令如下:

 RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);

然后我们继续根据RequestCode.REGISTER_BROKER去NameSrv搜索相关代码。
在这里插入图片描述
broker信息是存放在RouteInfoManager的,RouterInfoManager类如下所示:

public class RouteInfoManager {
    private static final Logger log = LoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
    private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
    ......
}

topicQueueTable:存放topic路由信息,对应于之前的brokerTopic。
brokerAddrTable:broker信息,包括集群名称、broker名称、id与ip地址映射。
clusterAddrTable:集群信息。
brokerLiveTable:broker心跳信息,broker每隔一段时间就会和NameSrv发起心跳,获取最新的路由信息,并确认broker是否还存活。
filterServerTable:过滤相关,非重点。

注册broker信息,其实就是填充这几个表。因为有集群环境,因此注册上述表的时候,还需要添加写锁,具体代码这里就不贴了。
现在我们来复盘一下目前的过程,创建一个topic,首先向集群中的所有broker master注册topic信息。master broker拿到topic信息,保存到本地,然后再向所有的NameSrv结点发起注册broker信息请求。NamerSrv拿到topic和broker信息后,更新表(并没有持久化)。

Slave结点怎么办?

在集群模式下,Slave结点的topic路由信息是通过master结点同步过来的。在BrokerController的初始化时,如果是slave结点,会启动一个定时任务,每分钟从master结点同步路由信息。

 if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
                if (this.messageStoreConfig.getHaMasterAddress() != null && this.messageStoreConfig.getHaMasterAddress().length() >= 6) {
                    this.messageStore.updateHaMasterAddress(this.messageStoreConfig.getHaMasterAddress());
                    this.updateMasterHAServerAddrPeriodically = false;
                } else {
                    this.updateMasterHAServerAddrPeriodically = true;
                }

                this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

                    @Override
                    public void run() {
                        try {
                            BrokerController.this.slaveSynchronize.syncAll();
                        } catch (Throwable e) {
                            log.error("ScheduledTask syncAll slave exception", e);
                        }
                    }
                }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);

Slave结点的同步器做了如下工作,包括同步topic路由信息,消息消费偏移、group信息等。

public void syncAll() {
        this.syncTopicConfig();
        this.syncConsumerOffset();
        this.syncDelayOffset();
        this.syncSubscriptionGroupConfig();
    }

Slave结点拿到路由信息后,同样保存在本地,然后定时向所有NameSrv注册broker信息。这样,很短的一段时间过后,NameSrv就包含了所有的topic路由信息。最后再放上之前的图片,加深理解:
在这里插入图片描述

发布了379 篇原创文章 · 获赞 85 · 访问量 59万+

猜你喜欢

转载自blog.csdn.net/GAMEloft9/article/details/99600973
今日推荐