RocketMq源码解析三:路由管理

news2024/9/24 11:23:57

        Nameserver的主要作用是为消息的生产者和消息消费者提供关于主题Topic的路由信息,那么Nameserver需要存储路由的基础信息,还要管理Broker节点,包括路由注册、路由删除等。

一、路由元数据

        路由元数据主要保存了topic信息,broker信息等

        代码:RoutelnfoManager

        我们先看下RoutelnfoManager中的五个map,分别存储了哪些元数据。

    private final HashMap<String/* topic */, Map<String /* brokerName */ , 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:**Topig消息队列路由信息,消息发送时根据路由表进行负载均衡brokerAddrTable:Broker基础信息,包括brokerName、所属集群名称、主备Broker地址clusterAddrTable:Broker集群信息,存储集群中所有Broker名称

brokerLiveTable:Broker状态信息,NameServer每次收到心跳包是会替换该信息filterserverTable:Broker上的FilterServer列表,用于类模式消息过滤

      topicQueueTable的json结构如下:

topicQueueTable : {
"topic1": [
{
    "brokerName":"broker-a",
    "readoueueNums":4,
    "writeQueueNums":4,
    "perm":6,
    "topicSynFlag":0
},
{
    "brokerName":"broker-b",
    "readoueueNums":4,
    "writeQueueNums":4,
    "perm":6,
    "topicSynFlag":0
}],
"topic other":[]
}

        上述维护了主题topic与broker之间的关系。但是我们看到这里只有broker的名字,那么怎么找到这个broker呢。就需要brokerAddrTable来维护,我们看下brokerAddrTable的基本结构

brokerAddrTable :{
"broker-a": [
{
"cluster":"c1",
"brokerName" :"broker-a",
"brokerAddrs" :{0:"192.168.56.1:10000",1:"192.168.56.2:10000"
},
"broker-b":
{
"cluster":"cl",
"brokerAddrs" :{0:"192.168.56.3:10000",1:"192.168.56.4:10000"
}
}

        上述结构就维护了broker的地址和端口号。我们再看下clusterAddrTable,也就是集群信息的基本结构,很简单就不详细说明了

clusterAddrTable:{"CH":[{"broker-a", "broker-b" }]

        下面,就是broker的状态信息,可用还是不可用,brokerLiveTable的结构如下

brokerLiveTable :{
"192.168.56.1:10000":
{
"lastUpdateTimestamp":1518270318980,
"dataVersion":version0bl,
"channel":channelobj,
"haServerAddr":"192.168.56.2:10000"
},
"192.168.56.2:10000":{...},
...
}

        上述维护了一个broker的心跳信息,包括上次心跳的发送时间戳,netty的channel信息等。

        小结

         RocketMQ 基于订阅发布机制, 一个Topic 拥有多个消息队列,一个Broker 为每一主题默认创建4 个读队列4 个写队列。多个Broker 组成一个集群, BrokerName 由相同的多台Broker组成Master-Slave 架构, brokerId 为0 代表Master, 大于0 表示Slave 。BrokerLivelnfo 中的lastUpdateTimestamp 存储上次收到Broker 心跳包的时间。

二、路由注册

2.1 broker启动流程

       路由的注册由broker完成。我们通过源码来看下broker启动的的逻辑。和nameserver启动流程相似,broker启动后也是要创建一个BrokerController,大致的流程如下图所示。

        这里贴一部分源码        

public static BrokerController createBrokerController(String[] args) {
      
     ......
     
     // 创建三个config对象 BrokerConfig NettyServerConfig NettyClientConfig 
     final BrokerConfig brokerConfig = new BrokerConfig();
     final NettyServerConfig nettyServerConfig = new NettyServerConfig();
     final NettyClientConfig nettyClientConfig = new NettyClientConfig();

     // 给config对象赋值
     // 设定服务端口
     ......
     nettyServerConfig.setListenPort(10911);
    
     // 通过构造函数创建BrokerController 实例

     final BrokerController controller = new BrokerController(
                brokerConfig,
                nettyServerConfig,
                nettyClientConfig,
                messageStoreConfig);
     // remember all configs to prevent discard
     controller.getConfiguration().registerConfig(properties);

     // 调用初始化方法
     boolean initResult = controller.initialize();
     if (!initResult) {
         controller.shutdown();
         System.exit(-3);
     }


     //  钩子函数,等待jvm停止时调用
     Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                private volatile boolean hasShutdown = false;
                private AtomicInteger shutdownTimes = new AtomicInteger(0);

                @Override
                public void run() {
                    synchronized (this) {
                        log.info("Shutdown hook was invoked, {}", this.shutdownTimes.incrementAndGet());
                        if (!this.hasShutdown) {
                            this.hasShutdown = true;
                            long beginTime = System.currentTimeMillis();
                            controller.shutdown();
                            long consumingTimeTotal = System.currentTimeMillis() - beginTime;
                            log.info("Shutdown hook over, consuming total time(ms): {}", consumingTimeTotal);
                        }
                    }
                }
            }, "ShutdownHook"));

            return controller;
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(-1);
        }

        return null;
    }

           initialize()这个方法我们后面再详细说明。现在我们的brokerController已经创建完成,我们看源码中的后续调用逻辑。

    public static void main(String[] args) {
        start(createBrokerController(args));
    }

    public static BrokerController start(BrokerController controller) {
        try {

            controller.start();
            ...
        }

        return null;
    }

        从上面贴出的代码可以看到,后续borker调用了brokerController的start方法。在这个方法中就是broker向namesrv注册的过程。 

2.2 路由的注册

       brokerController的start方法中有如下代码:

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);

        上述代码就是设定了一个定时任务,brokerConfig.getRegisterNameServerPeriod()的默认值是30s,所以默认每个30秒回去注册broker的信息。我们进入registerBrokerAll的代码内部

    public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
        TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();

        if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission())
            || !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) {
            ConcurrentHashMap<String, TopicConfig> topicConfigTable = new ConcurrentHashMap<>();
            for (TopicConfig topicConfig : topicConfigWrapper.getTopicConfigTable().values()) {
                TopicConfig tmp =
                    new TopicConfig(topicConfig.getTopicName(), topicConfig.getReadQueueNums(), topicConfig.getWriteQueueNums(),
                        this.brokerConfig.getBrokerPermission());
                topicConfigTable.put(topicConfig.getTopicName(), tmp);
            }
            topicConfigWrapper.setTopicConfigTable(topicConfigTable);
        }

        if (forceRegister || needRegister(this.brokerConfig.getBrokerClusterName(),
            this.getBrokerAddr(),
            this.brokerConfig.getBrokerName(),
            this.brokerConfig.getBrokerId(),
            this.brokerConfig.getRegisterBrokerTimeoutMills())) {
            doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
        }
    }

        上述代码经过一系列的参数检查和封装,最后调用了 doRegisterBrokerAll 方法。

 private void doRegisterBrokerAll(boolean checkOrderConfig, boolean oneway,
        TopicConfigSerializeWrapper topicConfigWrapper) {
        // 调用brokerOuterAPI的registerBrokerAll方法去注册
        List<RegisterBrokerResult> registerBrokerResultList = this.brokerOuterAPI.registerBrokerAll(
     ...

}

        进入跟进doRegisterBrokerAll 方法内部。主要有一下几个步骤

1、获得namesrv的地址列表

2、设置请求头信息

3、遍历所有的namesrv

4、注册

public List<RegisterBrokerResult> registerBrokerAll(...) {
        
        // 获得namesrv的地址列表
        final List<RegisterBrokerResult> registerBrokerResultList = new CopyOnWriteArrayList<>();
        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);
            requestHeader.setClusterName(clusterName);
            requestHeader.setHaServerAddr(haServerAddr);
            requestHeader.setCompressed(compressed);

            ......

            // 遍历所有的namesrv
            final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
            for (final String namesrvAddr : nameServerAddressList) {
                brokerOuterExecutor.execute(() -> {
                    try {
                        // 注册
                        RegisterBrokerResult result = registerBroker(namesrvAddr, oneway, timeoutMills, requestHeader, body);
                        if (result != null) {
                            registerBrokerResultList.add(result);
                        }

                        log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                    } catch (Exception e) {
                        log.warn("registerBroker Exception, {}", namesrvAddr, e);
                    } finally {
                        countDownLatch.countDown();
                    }
                });
            }

            try {
                countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
            }
        }

        return registerBrokerResultList;
    }

小结

broker从启动到注册基本流程如下:

1、初始化BrokerController

  • 通过配置文件或者命令行获得配置参数创建三个配置对象,BrokerConfig、 NettyServerConfig、NettyClientConfig;
  • 通过构造函数创建出BrokerController的实例;
  • BrokerController的实例初始化(有大量的定时任务启动);
  • 预设钩子函数,当jvm结束时调用BrokerController实例的shutdown方法;

2、调用BrokerController实例的start方法注册路由

    内部注册了一个定时任务,默认每个30s向namesrv集群注册信息,也就是发送心跳。

2.3 处理请求包

        好的,现在broker端的心跳上报已经结束了,此时我们要看下namesrv中的处理请求包的过程。nameserver模块中处理心跳的类叫DefaultRequestProcessor.class,位置如下图所示:

         在其processRequest(ChannelHandlerContext ctx,RemotingCommand request) 方法中,会根据请求的RequestCode来匹配不同的处理方法。此时我们看到匹配注册的code如下:

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);
      }

        这里有一个判定,borker的版本号是否大于了3.0.11,这里又学到了一招如何兼容低版本。registerBrokerWithFilterServer和registerBroker都会调用最终的注册方 —namesrvController .getRouteInfoManager().registerBroker,如下:

RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
            requestHeader.getClusterName(),
            requestHeader.getBrokerAddr(),
            requestHeader.getBrokerName(),
            requestHeader.getBrokerId(),
            requestHeader.getHaServerAddr(),
            topicConfigWrapper,
            null,
            ctx.channel()
        );

        RouteInfoManager.class的registerBroker方法是核心方法,我们看下具体这个方法做了什么工作。

{
...
this.lock.writeLock().lockInterruptibly();

// 更新clusterAddrTable
Set<String> brokerNames = this.clusterAddrTable.computeIfAbsent(clusterName, k -> new HashSet<>());
brokerNames.add(brokerName);

 boolean registerFirst = false;

// 更新brokerAddrTable
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
      registerFirst = true;
      brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
       this.brokerAddrTable.put(brokerName, brokerData);
}
 Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();

// 更新topicQueueTable
 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()) {
               this.createAndUpdateQueueData(brokerName, entry.getValue());
            }
         }
    }
}

// 更新brokerLiveTable
 BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
                        new BrokerLiveInfo(
                                System.currentTimeMillis(),
                                topicConfigWrapper.getDataVersion(),
                                channel,
                                haServerAddr));


// 更新filterServerTable
if (filterServerList != null) {
                    if (filterServerList.isEmpty()) {
                        this.filterServerTable.remove(brokerAddr);
                    } else {
                        this.filterServerTable.put(brokerAddr, filterServerList);
                    }
                }
}

        从上述代码中我们可以看到,就是更新了我们第一章路由元数据里面提到的五个基数元数据的信息,依次更新了clusterAddrTable、brokerAddrTable、topicQueueTable、brokerLiveTable和filterServerTable。

        至此,路由注册完成。

2.4 路由删除

        Broker每隔30s向Nameserver发送一个心跳包,心跳包包含BrokerId,Broker地址,Broker名称,Broker 所属集群名称、Broker 关联的 Filterserver列表。

        但是如果Broker宕机,Nameserver 无法收到心跳包,此时Nameserver如何来剔除这些失效的Broker呢?Nameserver会每隔10s扫描brokerLiveTable状态表,如果BrokerLive的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker连接,同时更新topicqueueTable、brokerAddrTable、brokerLiveTable、fiterserverTable。

        RocketMQ有两个触发点来删除路由信息:
        1、NameServer定期扫描brokerLiveTable检测上次心跳包与当前系统的时间差,如果时间超过120s,则需要移除broker。

        2、Broker在正常关闭的情况下,会执行unregisterBroker指令。

        这两种方式路由删除的方法都是一样的,就是从相关路由表中删除与该broker相关的信息。

        我们在上一章已经讲述了Nameserver启动后会有一个定时任务来扫描 brokerLiveTable状态表,核心代码如下:

    public int scanNotActiveBroker() {
        int removeCount = 0;
        Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, BrokerLiveInfo> next = it.next();
            long last = next.getValue().getLastUpdateTimestamp();
            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());

                removeCount++;
            }
        }

        return removeCount;
    }

        我们看到主要是获得最后一次上报的时间,加上120s,如果小于当前时间,就先关闭channel,然后从brokerLiveTable中移除这个broker的信息。最后调用onChannelDestroy方法,这个方法就是从topicqueueTable、brokerAddrTable、fiterserverTable这几个当中查找相关数据并删除。这里就不一一展开说明了。

2.5 路由发现

        RocketMO路由发现是非实时的,当Topic路由出现变化后,NameServer不会主动推送给客户端,而是由客户端定时拉取主题最新的路由。
        代码:NameServer:DefaultRequestProcessor#getRouteInfoByTopic

case RequestCode.GET_ROUTEINFO_BY_TOPIC:
     return this.getRouteInfoByTopic(ctx, request);

         进入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 = 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);
            }

            byte[] content;
            Boolean standardJsonOnly = requestHeader.getAcceptStandardJsonOnly();
            if (request.getVersion() >= Version.V4_9_4.ordinal() || (null != standardJsonOnly && standardJsonOnly)) {
                content = topicRouteData.encode(SerializerFeature.BrowserCompatible,
                    SerializerFeature.QuoteFieldNames, SerializerFeature.SkipTransientField,
                    SerializerFeature.MapSortField);
            } else {
                content = RemotingSerializable.encode(topicRouteData);
            }

            response.setBody(content);
            response.setCode(ResponseCode.SUCCESS);
            response.setRemark(null);
            return response;
        }

        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;
    }

三、小结

        总结下来就是下面这张图

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1701488.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝卓入选工信部2023年度“揭榜挂帅”项目

蓝卓“面向多元异构和应用快速开发演化的智能工厂操作系统解决方案”&#xff0c;凭借行业领先的平台技术能力以及数智赋能的硬核实力成功揭榜挂帅。 本次入选不仅代表了蓝卓又一次获得工信部权威专家及国家认可&#xff0c;更是“工厂操作系统”首次在国家层面获得表彰。 智能…

海外网红营销新趋势:“快闪式”营销如何迅速提升品牌曝光度

在当今数字化时代&#xff0c;海外网红营销已成为品牌迅速触达全球消费者、提升品牌曝光度和刺激销售的重要手段。其中&#xff0c;“快闪式”营销以其独特的时效性、创意性和互动性&#xff0c;成为品牌与海外网红合作的新趋势。本文Nox聚星将和大家探讨如何利用海外网红的影响…

【Go专家编程——内存管理——逃逸分析】

逃逸分析 逃逸分析&#xff08;Escape Analysis&#xff09;是指由编译器决定内存分配的位置&#xff0c;不需要程序员决定。 在函数中申请一个新的对象 如果分配在栈上&#xff0c;则函数执行结束后可自动将内存回收如果分配在堆上&#xff0c;则函数执行结束后可交给GC&…

二叉搜索树BST ——(C++)

本篇将会讲解有关二叉树的搜索原理&#xff0c;以及关于二叉搜索树的建立&#xff0c;以及二叉树搜索树的插入、删除和查找等基本操作。最后我们还会对二叉搜索树进行功能扩展&#xff0c;介绍有关搜索二叉树的 K 模型和 KV 模型。目录如下&#xff1a; 目录 1. 搜索二叉树 二叉…

决策树与机器学习实战【代码为主】

文章目录 &#x1f6f4;&#x1f6f4;引言&#x1f6f4;&#x1f6f4;决策树使用案例&#x1f6f4;&#x1f6f4;numpy库生成模拟数据案例&#x1f6f4;&#x1f6f4;决策树回归问题&#x1f6f4;&#x1f6f4;决策树多分类问题 &#x1f6f4;&#x1f6f4;引言 决策树是一种经…

Https自签名证书

openSSL下载 https://slproweb.com/products/Win32OpenSSL.html 1_整体流程 &#xff08;1&#xff09;https介绍 HTTPS 是 Hypertext Transfer Protocol Secure 的简称&#xff0c;是基于 SSL 加密方式的 HTTP 协议 &#xff08;2&#xff09;CA机构介绍 介绍&#xff1a…

PHP之fastadmin系统配置分组增加配置和使用

目录 一、实现功能&#xff1a;fasttadmin实现添加系统配置分组和添加参数、使用 二、添加分组 三、配置分组参数 四、最终存储位置 五、获取配置参数 一、实现功能&#xff1a;fasttadmin实现添加系统配置分组和添加参数、使用 二、添加分组 在字典配置中找到分组对应键值…

高效掌控速卖通自养号测评:成本、步骤、技巧全方位掌握

在跨境电商的汹涌浪潮中&#xff0c;速卖通犹如一颗璀璨的领航星&#xff0c;引领着无数寻求海外拓展的企业和商家驶向国际市场的广阔海域。从最初的C2C模式起步&#xff0c;速卖通历经蜕变&#xff0c;如今已华丽转身成为B2C跨境电商领域的翘楚&#xff0c;承载着无数中国卖家…

新业务 新市场 | 灵途科技新品亮相马来西亚亚洲防务展

5月6日&#xff0c;灵途科技携新品模组与武汉长盈通光电&#xff08;股票代码&#xff1a;688143&#xff09;携手参加第18届马来西亚亚洲防务展。首次亮相海外&#xff0c;灵途科技便收获全球客户的广泛关注&#xff0c;为公司海外市场开拓打下坚实基础。 灵途科技与长盈通共同…

探索Facebook:数字社交的新时代

Facebook&#xff0c;作为全球最大的社交网络平台之一&#xff0c;一直在引领着数字社交的发展潮流。随着科技的不断进步和社会的不断变迁&#xff0c;Facebook也在不断演进和创新&#xff0c;迎接着数字社交的新时代。本文将探索Facebook在数字社交领域的新发展&#xff0c;以…

fpga系列 HDL: 05 阻塞赋值(=)与非阻塞赋值(<=)

在Verilog硬件描述语言&#xff08;HDL&#xff09;中&#xff0c;信号的赋值方式主要分为两种&#xff1a;连续赋值和过程赋值。每种赋值方式有其独特的用途和语法&#xff0c;并适用于不同类型的电路描述。 1. 连续赋值&#xff08;Continuous Assignment,assign 和&#xf…

pycharm中无法激活conda虚拟环境

在windwos的cmd命令行中能激活&#xff0c;但是pycharm中无法激活虚拟环境&#xff0c;报错提示&#xff1a; 后来发现pycharm默认命令行是powershell&#xff0c;改成cmd重启pycharm命令行即可

windows安全配置

环境&#xff1a;本文我们以Windows Server 2012 R2为例&#xff0c;进行加固 Windows 不论什么版本&#xff0c;进行安全配置均包含以下两个常用维度 1、账户策略 ①密码策略 强制密码历史&#xff0c;建议设置为24个 密码最长使用期限&#xff0c;建议设置60天 密码最短…

Excel函数之SCAN

SCAN 语法 SCAN([initial_value], array, lambda(accumulator,value,calculation)) initial_value:用于设置累加器的初始值,即accumulator的初始值,该值为空时,array的首个值将直接赋值给累加器,并且该值将不参与函数运算array:需要进行循环计算的数组accumulator:累加…

HTML5 基本框架

HTML5基本的内容 文章目录 系列文章目录前言一、HTML5 基本框架二、具体框架结构三、知识补充总结 前言 HTML5的介绍&#xff1a; HTML5 是一种用于构建网页内容的标准化语言。它是 HTML&#xff08;超文本标记语言&#xff09;的第五个版本&#xff0c;引入了许多新的功能和特…

wordpress主题给网站增加一个版权声明区块代码分享

在数字化时代&#xff0c;网络上的信息传播变得越来越便捷&#xff0c;给人们生活和工作带来了极大的便利。然而&#xff0c;在这个过程中也产生了很多版权问题。为了更好地保护自己的版权&#xff0c;许多网站开始在其网页上添加版权声明。本文将探讨在网站上添加版权声明的重…

PageHelper分页查询时,count()查询记录总数与实际返回的数据数量不一致

目录 场景简介代码判断异常情况排查原因解决 场景简介 1、使用PageHelper进行分页查询 2、最终构建PageInfo对象时&#xff0c;total与实际数据量不符 代码判断 异常情况 排查 通过对比count()查询的SQL与查询记录的SQL&#xff0c;发现是PageHelper分页查询时省去了order b…

Object类——toString方法和equals方法

前言&#xff1a; 在java中&#xff0c;所有类都是有继承关系存在的&#xff0c;都默认继承Object类。当一个类继承了其他父类&#xff0c;它并不会直接继承Object类&#xff0c;但是它的父类若是没有其他继承关系也会默认继承Object类&#xff0c;子类也可以继续调用Object类…

无线麦克风哪个品牌音质最好,揭示麦克风什么牌子的音质效果好!

​随着科技的不断发展&#xff0c;无线领夹麦克风已经成为现代演讲、演出和采访中不可或缺的工具。这种小巧便携的设备&#xff0c;能够让我们摆脱线缆的束缚&#xff0c;自由地在舞台上或讲台上移动&#xff0c;同时保持声音的清晰和稳定。在这篇文章中&#xff0c;我们将介绍…