文章目录

  • Zookeeper 集群模式一共有三种类型的角色
  • 1. zookeeper启动时leader选举流程
    • 1.1 加载配置文件,设置基本信息
    • 1.2 指定快速选举算法,启动多级队列、线程
    • 1.3 发送本节点选票到队列,节点之间建立socket通讯
    • 1.4 各节点之间选票收发通信逻辑
    • 1.5 选票PK,选出leader
    • 1.6 其他节点进来自动变为flower
  • 2. leader和flower节点数据同步
  • 3. leader挂掉后,如何自动选举新的leader?
  • 4. Zookeeper 集群节点为什么要部署成奇数?
  • 5. Zookeeper 集群中的"脑裂"问题解决
    • 5.1 脑裂现象
    • 5.2 脑裂解决方案

Zookeeper 集群模式一共有三种类型的角色

  • Leader: 处理所有的事务请求(写请求),可以处理读请求,集群中只能有一个Leader
  • Follower:只能处理读请求,同时作为 Leader的候选节点,即如果Leader宕机,Follower节点 要参与到新的Leader选举中,有可能成为新的Leader节点。
  • Observer:只能处理读请求。不能参与选举

1. zookeeper启动时leader选举流程

选举流程图:

集群启动时,leader选举并不是一次选举就能成功的,会有两次选举。选举谁作为leader节点是根据myid和ZXID的值决定的,ZXID有优先决定权!

第一轮选举时,每个节点都会给自己投一票,并发送投票结果到其他节点(包括自己)。比如myid=1节点 和 myid=2的节点,第一轮选举他们分别投出格式为(myid,ZXID)的选票,分别是(1,0)和(2,0),各自收到对方的选票,但由于集群刚启动,数据还未被修改,事务id(ZXID)为0,这时会比较myid,默认会给myid比较大的节点投一票,让其作为leader。

这时发起第二轮选举,由于myid=1的节点经过比较,感觉myid=2的节点适合做leader,于是第二次它投myid=2的节点一票。myid=2的节点也觉得自己的myid比1大,也投自己一票,这样myid=2的节点就有了两票。此时节点个数的一半是1个,由于2>1,myid=2的节点得票数 > 节点半数,所以选举成功,myid=2的节点为leader节点,myid=1的节点为flower节点!

当集群中第三个节点加入进来时,发现已有leader,主动成为flower节点!

1.1 加载配置文件,设置基本信息

通过zookeeper源码看一下启动过程,下载zookeeper源码并编译后,通过脚本文件找到zookeeper的启动类QuorumPeerMain,进入Main方法中initializeAndRun方法中:

        protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {QuorumPeerConfig config = new QuorumPeerConfig();if (args.length == 1) {//解析传进来的配置文件,这个配置文件就是zoo.cfg中的内容config.parse(args[0]);}// purge:清除// 定时清除一些早期冗余的日志、快照等无用文件,防止占用内存太大DatadirCleanupManager purgeMgr = new DatadirCleanupManager(config.getDataDir(),config.getDataLogDir(),config.getSnapRetainCount(),config.getPurgeInterval());purgeMgr.start();if (args.length == 1 && config.isDistributed()) {// Distributed:集群// 如果zookeeper是集群模式,走下面的逻辑runFromConfig(config);} else {LOG.warn("Either no config or no quorum defined in config, running in standalone mode");// 如果zookeeper是单机,走下面的逻辑ZooKeeperServerMain.main(args);}}

zookeeper在解析配置文件时,会解析到zoo.cfg底部是否配置有其他的集群节点,我们在zoo.cfg中配置了集群,所以会进入 runFromConfig(config)方法中。runFromConfig封装了集群启动的所有事项,主要有以下几项

  1. 创建服务端连接对象,默认是NIO,推荐用netty
     //创建连接工厂,默认NIOcnxnFactory = ServerCnxnFactory.createFactory();//配置连接工厂参数cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), false);
  1. 为当前服务节点设置属性,包括zoo.cfg中的,也包括默认的一些属性,比如:设置zk默认内存数据、设置连接工厂 、设置默认选举参数为3等等!
 //初始化本地节点quorumPeer = getQuorumPeer();//设置属性,太多。。。省略部分quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory()));//设置默认内存数据 /zookeeper/conf。。quorumPeer.setCnxnFactory(cnxnFactory); //设置连接工厂quorumPeer.setElectionType(config.getElectionAlg());//设置默认选举参数为3。。。。
  1. 启动服务节点
     quorumPeer.start();

1.2 指定快速选举算法,启动多级队列、线程

start方法如下:

    @Overridepublic synchronized void start() {if (!getView().containsKey(myid)) {throw new RuntimeException("My id " + myid + " not in the peer list");}//启动时先加载dir目录下的数据loadDataBase();//启动之前设置的连接工厂startServerCnxnFactory();try {//启动了一个JettyAdminServer服务器,用来存放zk服务器上的信息//使用localhost:8080/commands  可以访问adminServer.start();} catch (AdminServerException e) {LOG.warn("Problem starting AdminServer", e);System.out.println(e);}//指定选举算法,创建几个后台线程、收发队列,启动收发监听器等 初始化选举相关数据!startLeaderElection();startJvmPauseMonitor();//启动leader选举线程  进入本类的run方法 **重点** 选举逻辑都在这里super.start();}========================startLeaderElection()核心如下==================//后台启动一个监听器QuorumCnxManager.Listener listener = qcm.listener;if (listener != null) {//启动监听器,通过BIO方式启动一个选举socket,通过zoo.cfg中最后配置的端口进行通信listener.start();//选择快速选举算法,创建两个收发选票的阻塞队列FastLeaderElection fle = new FastLeaderElection(this, qcm);//启动收发守护线程fle.start();

startLeaderElection()方法中为选举做好了准备,启动了一个监听器listener.start(),监听器中创建了两个选举传输层的收发队列、收发线程!如下所示:

选举传输层:

  • ①:发送线程:SendWorker
  • ②:接收线程:RecvWorker
  • ①:发送队列:queueSendMap
  • ②:接收队列:recvQueue

代码如下:

//再次建立收发线程:SendWorker、RecvWorker SendWorker sw = new SendWorker(sock, sid);RecvWorker rw = new RecvWorker(sock, din, sid, sw);sw.setRecv(rw);SendWorker vsw = senderWorkerMap.get(sid);if (vsw != null) {vsw.finish();}//绑定远端ip  发送线程  保存到mapsenderWorkerMap.put(sid, sw);//绑定远端ip  队列   保存到mapqueueSendMap.putIfAbsent(sid, new CircularBlockingQueue<>(SEND_CAPACITY));/***   这些队列和线程 才是真正把选票发送到对方节点的操作*///启动收发线程sw.start();rw.start();

启动收发线程时,会把选票结果转化成流通过socket传输,分别查看收发线程中的run方法,如下所示:

sw.start()核心代码:

     //dout:OutputStream//发送时通过流的形式发送数据dout.writeInt(b.capacity());dout.write(b.array());dout.flush();

rw.start()核心代码:

  //din:DataInputStream // 读取到SendWorker从queueSendMap发过来的数据流din.readFully(msgArray, 0, length);// 把数据放入recvQueue中addToRecvQueue(new Message(ByteBuffer.wrap(msgArray), sid));

FastLeaderElection(this, qcm)中又创建了两个 选举应用层的收发线程,两个收发阻塞队列并启动!

选举应用层:

  • ①:发送线程:WorkerSender
  • ②:接收线程:WorkerReceiver
  • ①:发送队列:sendqueue
  • ②:接收队列:recvqueue
        //创建一个发选票的阻塞队列sendqueue = new LinkedBlockingQueue<ToSend>();//创建一个收选票的阻塞队列recvqueue = new LinkedBlockingQueue<Notification>();
        //发送选票线程this.ws = new WorkerSender(manager);this.wsThread = new Thread(this.ws, "WorkerSender[myid=" + self.getId() + "]");this.wsThread.setDaemon(true);//接收选票线程this.wr = new WorkerReceiver(manager);this.wrThread = new Thread(this.wr, "WorkerReceiver[myid=" + self.getId() + "]");this.wrThread.setDaemon(true);

1.3 发送本节点选票到队列,节点之间建立socket通讯

其中 super.start()方法是真正开始选举的方法,跟进去看一下,只取部分核心选举代码

       //开始选举synchronized (this) {// 变更投票周期 从 0 自增为 1logicalclock.incrementAndGet();//初始化选票 第一次默认投给自己updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());}//省略部分代码.......//本节点构建选票,//通过获取配置文件中的zookeeper集群内容,拿到其他节点的节点和通信端口//给别的机器发送选票通知消息sendNotifications();while ((self.getPeerState() == ServerState.LOOKING) && (!stop)) {// 注意:当前节点不仅可以发选票,也可以收选票   //本节点从recvqueue队列中拿别的节点发来的消息Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);//机器刚启动 是收不到选票的,所以n=nullif (n == null) {//返回falseif (manager.haveDelivered()) {sendNotifications();} else {// 建立socket连接远程机器manager.connectAll();}

注意:以上代码每个zookeeper启动时都会执行,第一轮投票投给自己,并发送选票。发选票时并不直接把选票发送给其他节点,而是把选票放在选举应用层发送队列sendqueue中,由选举应用层WorkerSend线程(发消息子线程)代替发送。但WorkerSend线程还并不是直接把选票发送到其他节点,接着往下看!!

各节点不仅要发送选票,也要接收其他节点的选票。接收也是由wrThread(收消息子线程)从接收队列recvqueue中拿到选票给当前节点。在集群刚启动时,各节点还未建立连接,如果recvqueue中为空,各节点会先尝试建立socket连接后再收发选票!

1.4 各节点之间选票收发通信逻辑

zookeeper在选票收发时,采用的多线程、多级队列的方式来进行处理的。作为基础架构,充分考虑了性能和解耦,值得肯定。

上面说到,发选票时,第一轮投票会把投给自己的票放(offer)到选举应用层的发送队列sendqueue中,由选举应用层发送线程WorkerSend(发消息子线程)代替发送,发送到哪里了呢?发到了选举传输层的发送队列queueSendMap中去,再由选举传输层的发送线程SendWorker从queueSendMap获取到数据,最后再把数据转化成流通过outputStream传输出去。发送逻辑结束!

其他节点接受选票时,由选举传输层接收线程RecvWorker接收到选举传输层的SendWorker发来的数据,把数据放在选举传输层的队列recvQueue中去。再由选举应用层接收线程WorkerReceiver从recvQueue中获取数据,再放入选举应用层接收队列recvqueue中去,zookeeper节点就可以从recvqueue队列中获取(poll)到选票了!

具体的流程图如下:

问题一:选举传输层为什么要搞多个队列/线程,每个队列/线程绑定一个远程节点sid?

首先每个队列/线程绑定一个远程节点id,保证在线程从队列中取数据互不影响。每个节点对应的线程去这个节点的队列中获取选票信息,多节点/线程可以防止某个节点出了问题,所有节点跟着阻塞的情况发生。

1.5 选票PK,选出leader

1.3节中提到当前节点第一轮会给自己投票,并发送选票,同时也会接受选票。接收选票时,由于机器刚启动时,接受选票队列为null,各机器要先建立连接。在通信连接建立起来之后,接收选票队列中会有其他服务器节点发来的选票,当前节点拿接收到的票和自己上次投的票作对比PK,保存胜利的票,并在第二次选举时发出去,那么核心逻辑选票PK是怎么做的呢?进入源码分析

首先会比较选举次数electionEpoch

    // electionEpoch:选举次数,原子量,每次选举都会 +1// 比较选举次数  拿接收到的 和 自己的比较 ,第一次肯定都是1,是相等的,不会进入这里if (n.electionEpoch > logicalclock.get()) {logicalclock.set(n.electionEpoch);.....}//totalOrderPredicate() 选票PK方法else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {//第一次会进入这里updateProposal(n.leader, n.zxid, n.peerEpoch);sendNotifications();}

由于都是第一次选举,选举次数一样,所以会进入totalOrderPredicate() 选票PK方法

     # 选票PK核心逻辑return ((newEpoch > curEpoch)# 比较选举次数,如果选举次数相等|| ((newEpoch == curEpoch)# 比较事务ID Zxid&& ((newZxid > curZxid)|| ((newZxid == curZxid)# 如果事务ID相等,再比较myid&& (newId > curId)))));

选票PK结束之后,更新选票结果,并进行第二轮选举

     //更新选票updateProposal(n.leader, n.zxid, n.peerEpoch);//发送选票sendNotifications();

接下来,会把各节点选票结果放入一个set中,判断其是否大于集群半数

    public boolean containsQuorum(Set<Long> ackSet) {//是否大于集群半数return (ackSet.size() > half);}

如果第二轮选票大于集群半数,则设置选举结果,Leader就选举出来了

    //设置选举结果setPeerState(proposedLeader, voteSet);=================================== setPeerState ===============private void setPeerState(long proposedLeader, SyncedLearnerTracker voteSet) {//如果当前节点pk赢了,设置为leader,如果输了 设置为flowerServerState ss = (proposedLeader == self.getId()) ? ServerState.LEADING : learningState();self.setPeerState(ss);if (ss == ServerState.LEADING) {leadingVoteSet = voteSet;}}

注意:选举leader的整个过程是在一个while死循环中进行的,根据不同的节点状态执行不同的逻辑

节点状态有以下四种

    public enum ServerState {LOOKING,    //正在选举leaderFOLLOWING,  //flower节点LEADING,  //leafer节点OBSERVING //observe节点}

由于此时节点已经选出来了,再次进入while循环时,就会通过case分别进入不同的逻辑

     while (boolean){case LOOKING.....   //上面的选举逻辑在这里case OBSERVING:.....    //observe节点逻辑case FOLLOWING:.....   //flower节点逻辑case LEADING:.....  //leafer节点逻辑}

1.6 其他节点进来自动变为flower

经过上面的选举,节点2由于myid比节点1大,节点2已成为leader,节点1成为flower,此时节点3加入进来,具体的处理逻辑是什么样的呢?

如图所示,当节点3进来之后,会有以下几步判断

  1. 向其他节点发送选票,这个选票刚开始也是投的自己(3,0)
  2. 其他节点接受到这个选票,但此时已有leader节点,不会再进行节点PK,会直接把leader节点标识(2,0)发给节点3
  3. 节点3拿到leader节点,进行节点PK,先判断选举次数,(2,0)的选举次数由于经过了多次选举,次数是肯定大于(3,0)的
  4. 由于选举次数 节点2 > 节点3,节点3PK输掉,接下来会设置自己为节点2的flower节点,完成自动追随!

2. leader和flower节点数据同步

首先要明白zookeeper集群配置文件有三个节点:

clientPort=2191
server.1=192.168.100.100:2001:3001

如上代码

  1. 客户端、服务端通信端口:2191
  2. 集群节点间数据传输端口:2001
  3. 集群选举端口:3001

当leader选举出来后,要执行leader的职责,大致如下图

flower同样也根据此socket连接与leader通信

3. leader挂掉后,如何自动选举新的leader?

leader存活时,周期性的向flower节点发送ping命令

    while{//遍历所有的flower节点,周期性的向flower节点发送ping命令for (LearnerHandler f : getLearners()) {f.ping();}}

flower节点会通过socket在while循环中不断接收leader传来的命令。

     while (this.isRunning()) {//死循环不断从leader接收信息readPacket(qp);processPacket(qp);}

如果leader挂掉,flower节点接收不到消息,会抛异常,然后在finally中更新flower状态为 --> LOOKING,由于leader向所有的flower都发送ping命令,所以如果leader节点挂掉,所有节点都会变为LOOKING,会重新进行选举!

     case FOLLOWING:try {LOG.info("FOLLOWING");setFollower(makeFollower(logFactory));//不断接收leader节点follower.followLeader();} catch (Exception e) {LOG.warn("Unexpected exception", e);} finally {//如果出现异常,关闭flowerfollower.shutdown();setFollower(null);//更新flower状态为 --> LOOKING//如果leader节点挂掉,所有节点都会变为LOOKING,会重新进行选举updateServerState();}

4. Zookeeper 集群节点为什么要部署成奇数?

当宕掉几个zookeeper节点服务器之后,剩下的zk节点个数必须大于宕掉的个数,也就是剩下的节点服务数必须大于n/2,这样zookeeper集群才可以继续使用,无论奇偶数都可以选举leader。

例如5台zookeeper节点机器最多宕掉2台,还可以继续使用,因为剩下3台大于5/2。至于为什么最好为奇数个节点?这样是为了以最大容错服务器个数的条件下,能节省资源。比如,最大容错为2的情况下,对应的zookeeper服务数,奇数为5,而偶数为6,也就是6个zookeeper服务的情况下最多能宕掉2个服务,所以从节约资源的角度看,没必要部署6(偶数)个zookeeper服务节点。

集群中只要有过半的机器是正常工作的,那么整个集群对外就是可用的。也就是说如果有2个zookeeper节点,那么只要有1个zookeeper节点死了,那么zookeeper服务就不能用了,因为1没有过半,所以2个zookeeper的死亡容忍度为0;同理,要是有3个zookeeper,一个死了,还剩下2个正常的,过半了,所以3个zookeeper的容忍度为1;同理也可以多列举几个:2->0; 3->1; 4->1; 5->2; 6->2 就会发现一个规律,2n和2n-1的容忍度是一样的,都是n-1,所以为了更加高效,何必增加那一个不必要的zookeeper呢。所以说,根据以上可以得出结论:从资源节省的角度来考虑,zookeeper集群的节点最好要部署成奇数个!

5. Zookeeper 集群中的"脑裂"问题解决

5.1 脑裂现象

对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房:

正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。如下所示

        对于这种情况,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,就会产生数据该怎么合并,数据冲突怎么解决等等问题。这就相当于原本一个集群,被分成了两个集群,出现了两个"大脑",这就是所谓的"脑裂"现象

5.2 脑裂解决方案

zookeeper自带的半选举机制,可以解决脑裂问题。半数选举机制是指:只有大于集群节点个数的一半才能选出leader!

回到上文出现脑裂问题的场景:当机房中间的网络断掉之后,机房1内的三台服务器会进行 Leader选举,但是此时过半机制的条件是 “节点数 > 3”,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。

注意:半数选举机制是大于节点半数,而非大于等于!!
如果过半机制的条件是 “节点数 >= 3”,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。这就可以解释为什么过半机制中是大于而不是大于等于,目的就是为了防止脑裂。

六台机器部署在两个机房的脑裂问题,经过半数选举机制已经得到解决,结果是不满足半数,无法选举leader!下面看一下5台机器部署在两个机房是什么情况!

        此时过半机制的条件是 “节点数 > 2”,也就是至少要3台服务器才能选出一个Leader,此时机房件的网络断开了,对于机房1来说是没有影响的,Leader依然还是Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。

因此总结得出,有了过半机制,对于一个Zookeeper集群来说,要么没有Leader,要么只有1个Leader,这样zookeeper也就能避免了脑裂问题。

zookeeper的脑裂 和 redis的脑裂是两个问题:

  1. zookeeper由于有快速选举算法的半数选举机制,我们只需要均匀分配服务器上的zookeeper节点,即可避免脑裂。
  2. 而redis集群由于是向集群的其他master节点申请选票,通过半数选举选出master节点,但每个集群节点下的master-slave结构并没有半数选举机制,也就是说如果单个集群节点中的master由于假死复活,那么在单个集群节点master-slave结构下可能出现脑裂现象。只能通过配置的方式,尽可能的减少脑裂带来的数据丢失问题!

zookeeper专题:zookeeper集群模式下,leader选举流程分析相关推荐

  1. RSF-Center,集群模式下-协调数据结构

    为什么80%的码农都做不了架构师?>>>    RSF是一个轻量化的分布式服务框架.支持点对点调用,也支持分布式调用.典型的应用场景是,将同一个服务部署在多个Server上提供 re ...

  2. strom-1.1.0模拟单词统计功能,Spout编写,Bolt编写,TopologyDriver编写,本地模式运行,集群模式运行,集群模式下看输出结果

    统计文本中的单词出现的频率,其中文本内容如下: 创建项目 项目结构如下: 创建pom.xml,代码如下: <?xml version="1.0" encoding=" ...

  3. 如何访问集群中指定的服务器,【Nacos源码之配置管理 六】集群模式下服务器之间是如何互相感知的...

    前言 我们用Nacos当配置中心的时候,上一篇文章中 [Nacos源码之配置管理 五]为什么把配置文件Dump到磁盘中 知道了,所有的配置文件都会Dump到服务器的本地磁盘中, 那么集群模式下: 服务 ...

  4. 关于Redis集群模式下,使用mget通过keys批量获取value时的解决方案

    关于Redis集群模式下,使用mget通过keys批量获取value时的解决方案 今天在做项目的时候,需要使用到mget命令,通过一个批量的key去获取对应的value集合,但是取值的时候,报了这个错 ...

  5. quartz集群模式下qrtz_triggers表trigger_state变ERROR分析

    最近在正式环境新增了一个定时任务,项目启动后,新增的任务总是跑一两次就不跑了,排查发现trigger_state变为ERROR了. 一.Quartz重要表含义 1)qrtz_calendars:以Bl ...

  6. redis分布式锁 在集群模式下如何实现_收藏慢慢看系列:简洁实用的Redis分布式锁用法...

    在微服务中很多情况下需要使用到分布式锁功能,而目前比较常见的方案是通过Redis来实现分布式锁,网上关于分布式锁的实现方式有很多,早期主要是基于Redisson等客户端,但在Spring Boot2. ...

  7. Hadoop框架:集群模式下分布式环境搭建

    本文源码:GitHub·点这里 || GitEE·点这里 一.基础环境配置 1.三台服务 准备三台Centos7服务,基础环境从伪分布式环境克隆过来. 133 hop01,134 hop02,136 ...

  8. rocketMq双master集群模式下故障演练

    在上一篇,我们简单搭建了rocketMq双master的集群,沿用这个思路,这一篇我们用代码来模拟一下rocketMq集群故障情况下完成自动切换的效果. 1.启动两个节点的broker和nameser ...

  9. 集群模式下,redis锁的问题,红锁

    在使用redis来实现分布式锁的时候,如果redis是集群的,比如1主4从,这种主从模式就会存在延迟问题,导致加锁出现问题. 此时就应该使用红锁的方案,即在代码中不依赖于主从,将这5台机器视为平等的, ...

最新文章

  1. SAP MM 由于没有维护Plant的Address信息导致不能在ME51N和ME21N界面里输入工厂代码
  2. Python编程基础:第五十二节 高阶函数High Order Functions
  3. iPhone X适配小结
  4. 《鸿蒙理论知识04》HarmonyOS概述之系统定义
  5. Jenkins ssh 发布jar 时区不对
  6. Java NIO 非阻塞网络编程快速入门
  7. 有哪些在朋友圈发会被秒赞的文案?
  8. java 读取mysql数据库_原生Java操作mysql数据库过程解析
  9. Spring Cloud 入门手册
  10. Excel文件批量删除指定行或列
  11. px4直升机混控逻辑整理
  12. Vue表格绑定数据、添加数据
  13. windows画图板 ESL转RGB 实现区间渐变色
  14. ArcGIS 图像合并至新栅格图层(Mosaic To New Raster和Mosaic)
  15. yolov5的TensorRT部署--warpaffine_cuda核函数
  16. mxgate是gpcopy同步速度的2倍
  17. 简单工厂模式与工厂模式的区别
  18. 进击的JAVA freshman DAY01
  19. 锐捷交换机系统安装与升级
  20. 二开云海多功能解析系统全开源免授权4.5带插件

热门文章

  1. string中c_str()用法总结
  2. 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?
  3. linux安装telnet组件,LINUX下如何安装telnet
  4. Java中==和equals、equals和hashCode的关系详解
  5. springmvc进不到controller_Spring、SpringMVC、MyBatis的整合
  6. [Learn Notes] CSS学习笔记
  7. VBA打开TXT类文件读写相关操作代码
  8. 企业局域网内如何跨网安全传输数据
  9. c#自定义控件做漂亮的列表
  10. 饿了么口碑实现超30亿美元融资,引领本地生活数字化升级...