(三)Rocketmq的路由中心NameServer

一.NameServer架构设计

消息中间件的设计思路一般是基于主题的发布订阅机制实现的;Producer生产消息 ->消息服务器持久化->Cunsumer订阅消息消费,
为了避免单点故障,通常消息服务器是多台共同承担存储任务的,那么生产者如何知道消息该发送到那台消息服务器呢?如果一台消息服务器宕机了,那么生产者如何在不重启服务的情况下感知呢? NameServer就是为了解决上述问题而设计的, 与其类似的还有成熟的Zookeeper,但为什么Rocketmq没有采用我们后续揭晓!
在这里插入图片描述
Broker服务器在启动时向所有NameServer注册,Producer在发送消息前先从NameServer获取Broker服务器地址列表,然后根据负载均衡算法从列表中选择一台消息服务器进行消息发送;
NameServer与每台Broker保持长连接,通过建个30s的心跳检测确认存活,如果检测Broker宕机,则从路由注册表中将其移除,注意了,路有变化不会马上通知消息生产者,为什么要这样设计? 这是为了降低NameServer实现的复杂度,在消息发送端提供容错机制来保证消息发送的高可用性
NameServer本身的高可用通过部署多台NameServer服务器来实现的,但彼此之间互不通信,也就是存在NameServer之间某一时刻数据会不一致,但因为有容错机制,对消息发送不会造成影响,这也是NameServer设计的亮点,追求简单高效.

二.NameServer启动流程

从源码角度分析NameServer启动过程,重点关注相关启动参数
启动类位置:org.apache.rocketmq.namesrv.NamesrvStartup
NamesrvController controller = createNamesrvController(args);
start(controller);
String tip = “The Name Server boot success. serializeType=” + RemotingCommand.getSerializeTypeConfigInThisServer();
log.info(tip);
System.out.printf("%s%n", tip);
return controller;

step1:首先来解析配置文件,需要填充NameServerConfig NettyServerConfig属性值

org.apache.rocketmq.namesrv.NamesrvStartup#createNamesrvController
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
nettyServerConfig.setListenPort(9876);
if (commandLine.hasOption('c')) {
    String file = commandLine.getOptionValue('c');
    if (file != null) {
        InputStream in = new BufferedInputStream(new FileInputStream(file));
        properties = new Properties();
        properties.load(in);
        MixAll.properties2Object(properties, namesrvConfig);
        MixAll.properties2Object(properties, nettyServerConfig);
        namesrvConfig.setConfigStorePath(file);
        System.out.printf("load config properties file OK, %s%n", file);
        in.close();
    }
}
if (commandLine.hasOption('p')) {
    InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
    MixAll.printObjectProperties(console, namesrvConfig);
    MixAll.printObjectProperties(console, nettyServerConfig);
    System.exit(0);
}
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);

先创建NamesrvConfig 和 NettyServerConfig,然后再解析启动时指定的配置文件或者启动命令中的选项值,填充到nameserverconfig和nettyserverconfig对象中,参数来源有如下两种方式
1)-c configFile 通过-c命令指定配置文件的路径
2)使用"-- 属性名 属性值",eg “–listenPort 9876”

NamesrvConfig配置
/**
 * rocketmqHome:rocketmq主目录可通过-Drocketmq.home.dir=path 或者设置变量ROCKETMQ_HOME来设置rocketmq的主目录
 */
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
/**
 * NameServer存储KV配置属性的路径
 */
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
/**
 * NameServer默认配置文件路径,不生效,nameserver启动时如果要配置NameServer属性时,须通过-c配置
 */
private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
private String productEnvName = "center";
private boolean clusterTest = false;
/**
 * 是否开启消息顺序存储 默认不开启
 */
private boolean orderMessageEnable = false;
NettyServerConfig配置
/**
 * NameServer监听端口,默认值会被初始化成8888
 */
private int listenPort = 8888;
/**
 * netty业务线程数量
 */
private int serverWorkerThreads = 8;
/**
 * netty public任务线程数量
 */
private int serverCallbackExecutorThreads = 0;
/**
 * IO线程池线程个数,主要是NameServer Broker解析请求 返回相应的线程个数,主要处理网络请求的
 */
private int serverSelectorThreads = 3;
/**
 * send oneway消息请求并发度
 */
private int serverOnewaySemaphoreValue = 256;
/**
 * 异步消息发送最大并发度
 */
private int serverAsyncSemaphoreValue = 64;
/**
 * 网络连接最大空闲时间 默认120s 如果连接空闲时间超过该参数配置,连接将关闭
 */
private int serverChannelMaxIdleTimeSeconds = 120;
/**
 * 网络socket发送缓冲区大小 默认64kb
 */
private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
/**
 * 网络socket接收缓冲区大小 默认64kb
 */
private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
/**
 * ByteBuffer是否开启缓存 建议打开
 */
private boolean serverPooledByteBufAllocatorEnable = true;

/**
 * make make install
 * 是否启动Epoll IO模型 Linux环境建议开启
 * ../glibc-2.10.1/configure \ --prefix=/usr \ --with-headers=/usr/include \
 * --host=x86_64-linux-gnu \ --build=x86_64-pc-linux-gnu \ --without-gd
 */
private boolean useEpollNativeSelector = false;

step2:根据启动属性创建NamesrvController实例,并初始化该实例,NameServerController实例为NameServer核心控制器

org.apache.rocketmq.namesrv.NamesrvController#initialize
//加载KV配置
this.kvConfigManager.load();
//创建NettyServer网络处理对象
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

this.remotingExecutor =
    Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

this.registerProcessor();
//NameServer每隔10s扫描一次Broker,移除处于不激活状态的Broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        NamesrvController.this.routeInfoManager.scanNotActiveBroker();
    }
}, 5, 10, TimeUnit.SECONDS);
//nameserver每隔10分中打印一次KV配置
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        NamesrvController.this.kvConfigManager.printAllPeriodically();
    }
}, 1, 10, TimeUnit.MINUTES);

step3:注册JVM钩子函数并启动服务器,便于监听Broker,消息生产者的网络请求

小技巧,如果代码中使用了线程池,通过注册JVM钩子函数,在JVM进程关闭之前,先将线程池关闭,及时释放资源
//jvm钩子函数

Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
    @Override
    public Void call() throws Exception {
        controller.shutdown();
        return null;
    }
}));
controller.start();

JVM钩子函数更多了解->点击了解更多

三.路由注册及节点剔除

3.1路由信息

NameServer路由管理信息类:org/apache/rocketmq/namesrv/routeinfo/RouteInfoManager.java
首先看下有哪些关键属性

/**
 * Topic消息队列路由信息,消息发送时根据路由表进行负载均衡
 */
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
/**
 * Broker基础信息,包括brokerName 所属集群名称 主备Broker地址
 */
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
/**
 * Broker集群信息 存储集群中所有Broker名称
 */
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
/**
 * Broker状态信息,NameServer每次收到心跳包时会替换该信息
 */
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
/**
 * Broker上的FilterServer列表,用于类模式消息过滤
 */
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

一个Topic有多个消息队列,一个Broker为每个Topic创建4个读队列 4个写队列,多个Broker组成一个集群,BrokerName由多个相同的多台Broker组成Master-Slave架构,brokerid为0代表是Master,大于0位slave,BrokerLiveInfo中的lastUpdateTimestamp存储上次收到Broker心跳包的时间

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2路由注册

Rocketmq路由注册是通过Broker与NameServer的心跳功能实现的,Broker启动时向集群中所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,NameServer收到Broker心跳包时,更新brokerLiveTable中的lastUpdateTimestamp,如果连续120s,然后NameServer每隔10s扫描brokerLiveTable,如果连续120s没收到心跳包,NameServer将移除该Broker的路由信息同事关闭Socket连接

3.2.1Broker发送心跳包
org.apache.rocketmq.broker.BrokerController#start 注册到NameServer
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        try {
            BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
        } catch (Throwable e) {
            log.error("registerBrokerAll Exception", e);
        }
    }
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);

org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBrokerAll 遍历NameServer列表,依次发送心跳包

final List<RegisterBrokerResult> registerBrokerResultList = Lists.newArrayList();
//获取所有NameServer的地址
List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
    final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
    //封装请求头
    requestHeader.setBrokerAddr(brokerAddr);
    requestHeader.setBrokerId(brokerId);
    requestHeader.setBrokerName(brokerName);
    ......
    RegisterBrokerBody requestBody = new RegisterBrokerBody();
    requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper); //主题配置
    //消息过滤服务器列表
    requestBody.setFilterServerList(filterServerList);
    ......
    final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
    for (final String namesrvAddr : nameServerAddressList) {
        brokerOuterExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                    if (result != null) {
                        registerBrokerResultList.add(result);
                    }
......省略
    }


org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBroker 
if (oneway) {
    try {
        this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
    } catch (RemotingTooMuchRequestException e) {
        // Ignore
    }
    return null;
}
RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
.....省略 response结果的处理
3.2.2NameServer处理心跳包
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor网络处理器解析请求类型,如果请求类型为RequestCode.REGISTER_BROKER,则将请求最终发送到RouteInfoManager#registerBroker
case RequestCode.REGISTER_BROKER:
    Version brokerVersion = MQVersion.value2Version(request.getVersion());
    if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
        return this.registerBrokerWithFilterServer(ctx, request);
    } else {
        return this.registerBroker(ctx, request);
    }


org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker
try {
    //防止并发修改
    this.lock.writeLock().lockInterruptibly();
    //获取集群列表,如果不存在创建
    Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
    if (null == brokerNames) {
        brokerNames = new HashSet<String>();
        this.clusterAddrTable.put(clusterName, brokerNames);
    }
    brokerNames.add(brokerName);
    boolean registerFirst = false;
    //broker地址列表是否存在 不存在创建一个
    BrokerData brokerData = this.brokerAddrTable.get(brokerName);
    if (null == brokerData) {
        registerFirst = true;//第一次注册
        brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
        this.brokerAddrTable.put(brokerName, brokerData);
    }
    Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
    //Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
    //The same IP:PORT must only have one record in brokerAddrTable
    Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
    while (it.hasNext()) {
        Entry<Long, String> item = it.next();
        if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
            it.remove();
        }
    }

    String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
    //是否为第一次注册
    registerFirst = registerFirst || (null == oldAddr);
    //如果为master 且 topic初次配置或更新配置
    if (null != topicConfigWrapper
        && MixAll.MASTER_ID == brokerId) {
        if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
            || registerFirst) {
            ConcurrentMap<String, TopicConfig> tcTable =
                topicConfigWrapper.getTopicConfigTable();
            if (tcTable != null) {
                for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                    //创建或者更新topicQueueTable
                    this.createAndUpdateQueueData(brokerName, entry.getValue());
                }
            }
        }
    }
    //更新存活broker信息表 BrokerLiveInfo是执行路由删除的重要依据
    BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
        new BrokerLiveInfo(
            System.currentTimeMillis(),
            topicConfigWrapper.getDataVersion(),
            channel,
            haServerAddr));
    if (null == prevBrokerLiveInfo) {
        log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
    }
    //注册Broker的过滤器server地址列表,一个Broker上有多个FilterServer消息过滤器
    if (filterServerList != null) {
        if (filterServerList.isEmpty()) {
            this.filterServerTable.remove(brokerAddr);
        } else {
            this.filterServerTable.put(brokerAddr, filterServerList);
        }
    }
    //如果为从节点需 查询该对应的master节点更新masterAddr信息
    if (MixAll.MASTER_ID != brokerId) {
        String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
        if (masterAddr != null) {
            BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
            if (brokerLiveInfo != null) {
                result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
                result.setMasterAddr(masterAddr);
            }
        }
    }

设计亮点:NameServer与Broker建立长连接,Broker状态存储在BrokerLiveTable中,NameServer没收到一个心跳包,将更新brokerLiveTable关系与broker状态及topicQueueTable brokerAddrTable brokerLiveTable filterServerTable,更新时,使用读写锁,并发读,但同一时刻只处理一个Broker心跳包,多个心跳包并发执行

3.3路由删除

Rocketmq有两个触发点来触发路由删除
1)NameServer定时扫描brokerLiveTable检测上次心跳包与当前系统时间的时间差,如果时间戳大于120s,则需要移除该Broker信息
2)Broker在正常被关闭的情况等下,会执行unregisterBroker命令
不论以何种方式触发的路由删除,删除的方法都是一样的,就是从topicQueueTable brokerAddrTable brokerLiveTable filterServerTable删除与该Broker相关的信息
NameServer定时扫描的逻辑在NamesrvConfig启动中 org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#scanNotActiveBroker

public void scanNotActiveBroker() {
    Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, BrokerLiveInfo> next = it.next();
        long last = next.getValue().getLastUpdateTimestamp();
        //如果超过了120s
        if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
            RemotingUtil.closeChannel(next.getValue().getChannel());
            it.remove();
            log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
        }
    }
}

org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#onChannelDestroy

public void onChannelDestroy(String remoteAddr, Channel channel) {
    String brokerAddrFound = null;
    if (channel != null) {
        try {
            try {
                //申请读锁
                this.lock.readLock().lockInterruptibly();
                Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
                    this.brokerLiveTable.entrySet().iterator();
                //判断是否删除了
                while (itBrokerLiveTable.hasNext()) {
                    Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
                    if (entry.getValue().getChannel() == channel) {
                        brokerAddrFound = entry.getKey();
                        break;
                    }
                }
            } finally {
                this.lock.readLock().unlock();
            }
        } catch (Exception e) {
            log.error("onChannelDestroy Exception", e);
        }
    }

    if (null == brokerAddrFound) {
        brokerAddrFound = remoteAddr;
    } else {
        log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
    }

    if (brokerAddrFound != null && brokerAddrFound.length() > 0) {

        try {
            try {
                //申请写锁
                this.lock.writeLock().lockInterruptibly();
                //从列表直接删除
                this.brokerLiveTable.remove(brokerAddrFound);
                this.filterServerTable.remove(brokerAddrFound);
                String brokerNameFound = null;
                boolean removeBrokerName = false;
                Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
                    this.brokerAddrTable.entrySet().iterator();
                while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
                    BrokerData brokerData = itBrokerAddrTable.next().getValue();

                    Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
                    while (it.hasNext()) {
                        Entry<Long, String> entry = it.next();
                        Long brokerId = entry.getKey();
                        String brokerAddr = entry.getValue();
                        //删除broke节点
                        if (brokerAddr.equals(brokerAddrFound)) {
                            brokerNameFound = brokerData.getBrokerName();
                            it.remove();
                            log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
                                brokerId, brokerAddr);
                            break;
                        }
                    }

                    if (brokerData.getBrokerAddrs().isEmpty()) {
                        removeBrokerName = true;
                        itBrokerAddrTable.remove();
                        log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
                            brokerData.getBrokerName());
                    }
                }
                //删除集群列表
                if (brokerNameFound != null && removeBrokerName) {
                    Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
                    while (it.hasNext()) {
                        Entry<String, Set<String>> entry = it.next();
                        String clusterName = entry.getKey();
                        Set<String> brokerNames = entry.getValue();
                        boolean removed = brokerNames.remove(brokerNameFound);
                        if (removed) {
                            log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
                                brokerNameFound, clusterName);

                            if (brokerNames.isEmpty()) {
                                log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
                                    clusterName);
                                it.remove();
                            }

                            break;
                        }
                    }
                }
                //删除topic的列表
                if (removeBrokerName) {
                    Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
                        this.topicQueueTable.entrySet().iterator();
                    while (itTopicQueueTable.hasNext()) {
                        Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
                        String topic = entry.getKey();
                        List<QueueData> queueDataList = entry.getValue();

                        Iterator<QueueData> itQueueData = queueDataList.iterator();
                        while (itQueueData.hasNext()) {
                            QueueData queueData = itQueueData.next();
                            if (queueData.getBrokerName().equals(brokerNameFound)) {
                                itQueueData.remove();
                                log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
                                    topic, queueData);
                            }
                        }

                        if (queueDataList.isEmpty()) {
                            itTopicQueueTable.remove();
                            log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
                                topic);
                        }
                    }
                }
            } finally {
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("onChannelDestroy Exception", e);
        }
    }
}

3.4路由发现

Rocketmq路由发现不是实时的,当Topic路由出现变化后,NameServer不会主动推送给客户端,而是由客户端拉取主题最新的路由,根据主题名称拉群路由信息的命令编码是:GET_ROUTEINFO_BY_TOPIC

public class TopicRouteData extends RemotingSerializable {
    /**
     * 顺序消息配置内容
     */
    private String orderTopicConf;
    /**
     * topic队列元数据
     */
    private List<QueueData> queueDatas;
    /**
     * topic分布的broker数据
     */
    private List<BrokerData> brokerDatas;
    /**
     * broker上过滤服务器地址列表
     */
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

NameServer路由发现实现类的实现:org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final GetRouteInfoRequestHeader requestHeader =
        (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
    //构建TopicRouteData
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());

    if (topicRouteData != null) {
        //是否开启顺序消息
        if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
            String orderTopicConf =
                this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
                    requestHeader.getTopic());
            topicRouteData.setOrderTopicConf(orderTopicConf);
        }
        //使用fastjson进行的序列化
        byte[] content = topicRouteData.encode();
        response.setBody(content);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
        return response;
    }
    //如果为找到 设置TOPIC_NOT_EXIST
    response.setCode(ResponseCode.TOPIC_NOT_EXIST);
    response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
        + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
    return response;
}

四.个人思考总结

通过上述的学习了解 我们对NameServer有了初步的了解,从源码上看 NameServer实现较为简单, 通过水平扩展NameServer
不提供事件变更通知,NameServer之间是去中心化的,存在某一时刻NameServer之间数据短暂的不同,但可以保证最终一致, 这恰恰符合mq的设计的初衷,允许短暂的不一致,保证最终一致性

参考:Rocketmq技术内幕

发布了235 篇原创文章 · 获赞 221 · 访问量 96万+

猜你喜欢

转载自blog.csdn.net/drdongshiye/article/details/105320075