zookeeper 核心原理

1、了解zookeeper的设计

2、zookeeper集群角色

3、深入分析ZAB协议

4、从源码层面分析leader选举的实现过程

5、关于zookeeper的数据存储

6、关于zookeeper数据存储

zookeeper 的由来

zookeeper的设计

防止单点故障

集群方案(leader、follower)、还能分担请求

每个节点的数据是一致的

leader、master、redis-cluster

leader挂了怎么办?数据如何恢复

选举机制

如何去保证数据一致性?(分布式事务)

2PC

结论:

zab来实现选举:集群内选举leader来调度简化集群的复杂度,

为什么要做集群:保证zookeeper协调工具的高性能和高可用(热备,同步)

2pc做数据一致性:引入了协调者(leader)和参与者(follower)的概念。

zookeeper 集群

1. follower:处理读请求,转发写请求给leader

2. leader接收到事务请求后会转发提议给集群中的每一个节点(observer除外)

3. follwer节点收到提议后响应,返回ack

4. leader收到过半节点响应ack,便会提交事务(commit),给客户端一个response。反之会执行回滚。

5. 事务提交后会同步给Observer

3种角色特性:

leader:集群的核心,起到了主导整个集群的作用,事务请求的调度和处理。

follower:处理客户端的非事务请求,转发事务请求,参与事务的投票过程,参与leader选举投票

observer:观察者角色,了解集群中的状态变化,进行状态同步。可以响应非事务请求。

备注:observer与follwer工作原理一致,区别是不参与事务请求的投票,投票会影响性能。

当引入更多节点提升性能时候,多投票,多网络请求,但observer可以在不投票不增加网络请求的情况下提升性能,所以引入了observer。

Zookeeper集群中服务器数量的增加,会影响集群中写数据的性能,因为集群中是使用2PC协议,索引当更新节点的时候,需要半数已经的机器的ack才会执行commit操作。机器的增加,势必会增加收集ack的时间。Observer在不影响集群中事务处理能力的前提下,扩展Zookeeper提高集群中的非事务的处理能力。

1、观察zk集群的服务器的状态,并将状态同步到observer服务器上。

2、处理客户端的非事务请求,转发事务请求给leader

3、不参与任何投票(与follow的区别)

节点数:2n+1节点,至少n+1个可用,满足投票机制过半机制的需要,所以是最少三个,奇数节点。

server.1=192.168.1.103:2888:3181

server.2=192.168.1.104:2888:3181

server.3=192.168.1.106:2888:3181

server.4=192.168.1.102:2888:3181

ZAB 协议

ZAB(zookeeper atomic Broadcast)协议是为分布式协调服务zookeeper专门设计的一种支持崩溃恢复的原子广播协议,主要用于实现分布式数据一致性,通过主备模式的系统架构来保持集群中各个副本之间的数据一致性。

支持崩溃恢复的原子广播协议、主要用于实现数据一致性,

ZAB协议的两个基本模式,也是zab核心:

崩溃恢复

原子广播

注意:

投票是所有节点参与的,leader自己也不例外

但所有投票过程不需要observer ,但observer必须要和leader节点保持数据同步,保证正确的处理非事务请求。

消息广播

改进版2PC

崩溃恢复(对数据层来说)

1、当leader失去了过半的follower节点的联系

2、当leader服务挂了

​ 集群就会进入崩溃恢复阶段对于数据恢复来说

1、已经被处理的消息不能丢失

当leader收到合法数量的follower的ack以后,就会向各个follower广播消息(commit命令),同时自己也会commit这条事务消息,如果follower节点收到commit命令之前。leader挂了,会导致部分节点收到commit,部分节点没有收到,那么**zab协议需要保证已经被处理的消息不能丢失。**

2、被丢弃的消息不能再次出现

当leader收到事务请求,并且还未发起事务投票之前,leader挂了;怎么办?

旧的leader带领的上个朝代没有提交的事务会被全部丢弃。

**此时zab协议要保证被丢弃的消息不能再出现。**

**zab 的设计思想**

为了满足上面的两个原则,zab做了如下的设计:

1. zxid(消息id)是最大的。(新选举的leader的zxid是最大的,保证当前节点的消息是最新的)。

比如leader挂了之后,follwer1收到了commit请求,follwer1的zxid就是最新的,最大的,follwer2没有收到commit请求,zxid不是最大的,选举时候依旧选举zxid是最大的那个节点作为leader,follwer1的提交之前的commit请求可以保证数据时最新的,不丢失,由此满足了上面的第一条原则。

2. epoch的概念,每产生一个leader,那么新的leader的epoch会+1,zxid是64位的数据,低32位表示消息计数器(自增),高32位(存储epoch编号)。tips:epoch概念可以联想各个朝代皇帝的年号。

tips:

新选举的leader的epoch会比上一轮leader的epoch高,这样保证上一轮leader再起来之后本一轮不会被选举成为leader,而变成了一个follwer,而且旧的leader的zxid会小于新leader的zxid,新的leader继任之后会把旧的leader所有没提交的事务清除,由此满足了上面的第二条原则。

疑问如下:

2. 临时节点使用场景:分布式锁;既然有持久化节点;

3. 为什么需要有临时节点存在; 提升集群性能、客户端断开连接临时节点会自动删除,减少网络开销

leader选举

基于fastleader选举:

1、选举指标

zxid 最大设置为leader 64位,

myid(服务器id,sid)【myid越大,在leader选举机制中权重越大】

2、选举阶段

启动时

运行时崩溃后

epoch (每一轮投票,epoch都会递增)

选举状态

graph LR

LOKING[LOKING]--> LEADING[LEADING] --> FOLLOWING[FOLLOWING] --> OBSERVING[OBSERVING]

启动的时候初始化

(myid,zxid,epoch)

检查zxid

myid

统计投票

判断epoch

zxid

再判断myid

QuorumPeer

Leader选举源码分析

看QuormPeerMain的 main.initializeAndRun(args)

1 protected void initializeAndRun(String[] args) throwsConfigException, IOException, AdminServerException {2 //用来保存全局配置

3 QuorumPeerConfig config = newQuorumPeerConfig();4 if (args.length == 1) {5 //args[0] -> zoo.cfg, 解析配置文件并保存到 QuorumPeerConfig

6 config.parse(args[0]);7 }8 //Start and schedule the the purge task9 //启动一个定时任务清理日志

10 DatadirCleanupManager purgeMgr = newDatadirCleanupManager(11 config.getDataDir(),12 config.getDataLogDir(),13 config.getSnapRetainCount(),14 config.getPurgeInterval());15 purgeMgr.start();16 //判断是否standalone模式,或是集群

17 if (args.length == 1 &&config.isDistributed()) {18 runFromConfig(config);19 } else{20 LOG.warn("Either no config or no quorum defined in config, running in standalone mode");21 //there is only server in the quorum -- run as standalone

22 ZooKeeperServerMain.main(args);23 }24 }

看runFromConfig 是怎么处理的

1 protected void initializeAndRun(String[] args) throwsConfigException, IOException, AdminServerException {2 //用来保存全局配置

3 QuorumPeerConfig config = newQuorumPeerConfig();4 if (args.length == 1) {5 //args[0] -> zoo.cfg, 解析配置文件并保存到 QuorumPeerConfig

6 config.parse(args[0]);7 }8 //Start and schedule the the purge task9 //启动一个定时任务清理日志

10 DatadirCleanupManager purgeMgr = newDatadirCleanupManager(11 config.getDataDir(),12 config.getDataLogDir(),13 config.getSnapRetainCount(),14 config.getPurgeInterval());15 purgeMgr.start();16 //判断是否standalone模式,或是集群

17 if (args.length == 1 &&config.isDistributed()) {18 runFromConfig(config);19 } else{20 LOG.warn("Either no config or no quorum defined in config, running in standalone mode");21 //there is only server in the quorum -- run as standalone

22 ZooKeeperServerMain.main(args);23 }24 }

看runFromConfig 是怎么处理的

1 public void runFromConfig(QuorumPeerConfig config) throwsIOException, AdminServerException {2 try{3 ManagedUtil.registerLog4jMBeans();4 } catch(JMException e) {5 LOG.warn("Unable to register log4j JMX control", e);6 }7

8 LOG.info("Starting quorum peer, myid=" +config.getServerId());9 MetricsProvider metricsProvider; //指标数据

10 try{11 metricsProvider =MetricsProviderBootstrap.startMetricsProvider(12 config.getMetricsProviderClassName(),13 config.getMetricsProviderConfiguration());14 } catch(MetricsProviderLifeCycleException error) {15 throw new IOException("Cannot boot MetricsProvider " +config.getMetricsProviderClassName(), error);16 }17 try{18 ServerMetrics.metricsProviderInitialized(metricsProvider);19 //这个和2181端口监听有关系20 //ClientCnxn(客户端和服务端进行网络交互的类)21 //ServerCnxn(服务端网络通信处理类)

22 ProviderRegistry.initialize();23 ServerCnxnFactory cnxnFactory = null;24 ServerCnxnFactory secureCnxnFactory = null;25 //为客户端提供读写的server,也就是2181这个端口的访问功能

26 if (config.getClientPortAddress() != null) {27 cnxnFactory =ServerCnxnFactory.createFactory();28 cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), false);29 }30

31 if (config.getSecureClientPortAddress() != null) {32 secureCnxnFactory =ServerCnxnFactory.createFactory();33 secureCnxnFactory.configure(config.getSecureClientPortAddress(), config.getMaxClientCnxns(), config.getClientPortListenBacklog(), true);34 }35 //zk逻辑主线程、负责选举、投票

36 quorumPeer =getQuorumPeer();37 quorumPeer.setTxnFactory(newFileTxnSnapLog(config.getDataLogDir(), config.getDataDir()));38 quorumPeer.enableLocalSessions(config.areLocalSessionsEnabled());39 quorumPeer.enableLocalSessionsUpgrading(config.isLocalSessionsUpgradingEnabled());40 //quorumPeer.setQuorumPeers(config.getAllMembers());

41 quorumPeer.setElectionType(config.getElectionAlg()); //采用什么选举算法

42 quorumPeer.setMyid(config.getServerId()); //myId(sid,myid)

43 quorumPeer.setTickTime(config.getTickTime()); //心跳时间间隔(2000)

44 quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout());45 quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout());46 quorumPeer.setInitLimit(config.getInitLimit()); //数据初始化的时长

47 quorumPeer.setSyncLimit(config.getSyncLimit()); //数据同步时长

48 quorumPeer.setConnectToLearnerMasterLimit(config.getConnectToLearnerMasterLimit());49 quorumPeer.setObserverMasterPort(config.getObserverMasterPort());50 quorumPeer.setConfigFileName(config.getConfigFilename());51 quorumPeer.setClientPortListenBacklog(config.getClientPortListenBacklog());52 quorumPeer.setZKDatabase(newZKDatabase(quorumPeer.getTxnFactory()));53 quorumPeer.setQuorumVerifier(config.getQuorumVerifier(), false);54 if (config.getLastSeenQuorumVerifier() != null) {55 quorumPeer.setLastSeenQuorumVerifier(config.getLastSeenQuorumVerifier(), false);56 }57 quorumPeer.initConfigInZKDatabase(); //初始化内存数据库 ->磁盘持久化

58 quorumPeer.setCnxnFactory(cnxnFactory);59 quorumPeer.setSecureCnxnFactory(secureCnxnFactory);60 quorumPeer.setSslQuorum(config.isSslQuorum());61 quorumPeer.setUsePortUnification(config.shouldUsePortUnification());62 quorumPeer.setLearnerType(config.getPeerType());63 quorumPeer.setSyncEnabled(config.getSyncEnabled());64 quorumPeer.setQuorumListenOnAllIPs(config.getQuorumListenOnAllIPs());65 if(config.sslQuorumReloadCertFiles) {66 quorumPeer.getX509Util().enableCertFileReloading();67 }68 quorumPeer.setMultiAddressEnabled(config.isMultiAddressEnabled());69 quorumPeer.setMultiAddressReachabilityCheckEnabled(config.isMultiAddressReachabilityCheckEnabled());70 quorumPeer.setMultiAddressReachabilityCheckTimeoutMs(config.getMultiAddressReachabilityCheckTimeoutMs());71

72 //sets quorum sasl authentication configurations

73 quorumPeer.setQuorumSaslEnabled(config.quorumEnableSasl);74 if(quorumPeer.isQuorumSaslAuthEnabled()) {75 quorumPeer.setQuorumServerSaslRequired(config.quorumServerRequireSasl);76 quorumPeer.setQuorumLearnerSaslRequired(config.quorumLearnerRequireSasl);77 quorumPeer.setQuorumServicePrincipal(config.quorumServicePrincipal);78 quorumPeer.setQuorumServerLoginContext(config.quorumServerLoginContext);79 quorumPeer.setQuorumLearnerLoginContext(config.quorumLearnerLoginContext);80 }81 quorumPeer.setQuorumCnxnThreadsSize(config.quorumCnxnThreadsSize);82 quorumPeer.initialize();83

84 if(config.jvmPauseMonitorToRun) {85 quorumPeer.setJvmPauseMonitor(newJvmPauseMonitor(config));86 }87 //启动主线程

88 quorumPeer.start();89 ZKAuditProvider.addZKStartStopAuditLog();90 quorumPeer.join();91 } catch(InterruptedException e) {92 //warn, but generally this is ok

93 LOG.warn("Quorum Peer interrupted", e);94 } finally{95 if (metricsProvider != null) {96 try{97 metricsProvider.stop();98 } catch(Throwable error) {99 LOG.warn("Error while stopping metrics", error);100 }101 }102 }103 }

启动主线程,QuorumPeer 重写了 Thread.start 方法,我们接下来看start方法里面发生了什么?

调用 QUORUMPEER 的 START 方法

1 @Override2 public synchronized void start() { //重写线程start方法

3 if (!getView().containsKey(myid)) {4 throw new RuntimeException("My id " + myid + " not in the peer list");5 }6 loadDataBase(); //从磁盘加载数据

7 startServerCnxnFactory(); //这里来启动2181端口监听,ServerSocketChannel

8 try{9 adminServer.start();10 } catch(AdminServerException e) {11 LOG.warn("Problem starting AdminServer", e);12 System.out.println(e);13 }14 startLeaderElection(); //开启leader选举

15 startJvmPauseMonitor(); //启动监控

16 super.start(); //启动线程

17 }

loaddatabase, 主要是从本地文件中恢复数据,以及获取最新的 zxid

1 private voidloadDataBase() {2 try{3 zkDb.loadDataBase(); //从本地文件加载数据4

5 //load the epochs6 //从最新的zxid恢复epoch变量、zxid64位,前32位是epoch的值,后32位是zxid

7 long lastProcessedZxid =zkDb.getDataTree().lastProcessedZxid;8 long epochOfZxid =ZxidUtils.getEpochFromZxid(lastProcessedZxid);9 try{10 //从文件中读取当前的epoch

11 currentEpoch =readLongFromFile(CURRENT_EPOCH_FILENAME);12 } catch(FileNotFoundException e) {13 //pick a reasonable epoch number14 //this should only happen once when moving to a15 //new code version

16 currentEpoch =epochOfZxid;17 LOG.info(18 "{} not found! Creating with a reasonable default of {}. "

19 + "This should only happen when you are upgrading your installation",20 CURRENT_EPOCH_FILENAME,21 currentEpoch);22 writeLongToFile(CURRENT_EPOCH_FILENAME, currentEpoch);23 }24 if (epochOfZxid >currentEpoch) {25 throw new IOException("The current epoch, "

26 +ZxidUtils.zxidToString(currentEpoch)27 + ", is older than the last zxid, "

28 +lastProcessedZxid);29 }30 try{31 //从文件中读取接收的epoch

32 acceptedEpoch =readLongFromFile(ACCEPTED_EPOCH_FILENAME);33 } catch(FileNotFoundException e) {34 //pick a reasonable epoch number35 //this should only happen once when moving to a36 //new code version

37 acceptedEpoch =epochOfZxid;38 LOG.info(39 "{} not found! Creating with a reasonable default of {}. "

40 + "This should only happen when you are upgrading your installation",41 ACCEPTED_EPOCH_FILENAME,42 acceptedEpoch);43 writeLongToFile(ACCEPTED_EPOCH_FILENAME, acceptedEpoch);44 }45 if (acceptedEpoch

47 +ZxidUtils.zxidToString(acceptedEpoch)48 + " is less than the current epoch, "

49 +ZxidUtils.zxidToString(currentEpoch));50 }51 } catch(IOException ie) {52 LOG.error("Unable to load database on disk", ie);53 throw new RuntimeException("Unable to run quorum server ", ie);54 }55 }

退出loaddatabase,我们看初始化 LEADERELECTION

1 public synchronized voidstartLeaderElection() {2 try{3 //得到当前节点的状态,如果是 LOOKING

4 if (getPeerState() ==ServerState.LOOKING) {5 //构建一个Vote(myid、zxid、epoch)

6 currentVote = newVote(myid, getLastLoggedZxid(), getCurrentEpoch());7 }8 } catch(IOException e) {9 RuntimeException re = newRuntimeException(e.getMessage());10 re.setStackTrace(e.getStackTrace());11 throwre;12 }13 //根据electionType来创建选举算法

14 this.electionAlg =createElectionAlgorithm(electionType);15 }

配置选举算法,选举算法有 3 种,可以通过在 zoo.cfg 里面进行配置,默认是 fast 选举

1 protected Election createElectionAlgorithm(intelectionAlgorithm) {2 Election le = null;3

4 //TODO: use a factory rather than a switch

5 switch(electionAlgorithm) {6 case 1:7 throw new UnsupportedOperationException("Election Algorithm 1 is not supported.");8 case 2:9 throw new UnsupportedOperationException("Election Algorithm 2 is not supported.");10 case 3:11 //cnxn(和网络有关的一个类,ServerCnxn、ClientCnxn)12 //QuorumCnxManager 管理集群选举和投票相关的操作

13 QuorumCnxManager qcm =createCnxnManager();14 QuorumCnxManager oldQcm =qcmRef.getAndSet(qcm);15 if (oldQcm != null) { //判断是否已经开启选举

16 LOG.warn("Clobbering already-set QuorumCnxManager (restarting leader election?)");17 oldQcm.halt(); //种植掉当前的选举

18 }19 //监听集群中的票据

20 QuorumCnxManager.Listener listener =qcm.listener;21 if (listener != null) {22 listener.start();23 //初始化了FastLeaderElection

24 FastLeaderElection fle = new FastLeaderElection(this, qcm);25 fle.start(); //启动leader选举

26 le =fle;27 } else{28 LOG.error("Null listener when initializing cnx manager");29 }30 break;31 default:32 assert false;33 }34 returnle;35 }

启动leader选举

1 voidstart() {2 /**

3 * 启动两个线程4 * wsThread 业务层发送线程,将消息发送给IO负责类 QuorumCnxManger5 * 启动业务层接受线程,从IO负责类 QuorumCnxManger 接收消息6 */

7 this.wsThread.start();8 this.wrThread.start();9 }

投票过程

1、检查节点状态是looking时投票给自己、投票逻辑源码在FastLeaderElection类的lookforleader(),可以自己再细看源码

各个节点互相广播vode信息(myid,zxid,epoch)

先判断epoch,再判断zxid,再判断myid

胜出的投票会更新到当前的结果中。

继续广播,让其他节点知道自己现在的票据(告诉别人胜出的那个票据信息)。

epoch更新,进行下一轮选举

如果收到的消息epoch小于当前节点的epoch,则忽略这条消息(忽略旧的投票参数)

epoch相同时比较zxid,myid,如果胜出就更新自己的票据,并发出广播

投票的结果都蠢到本机的投票集合中,用来判断是不是超过半数

// 1、判断消息里的epoch是不是比当前的大,如果大则消息中id对应的服务器就是leader

// 2、如果epoch相等则判断zxid,如果消息里的zxid大,则消息中id对应的服务器就是leader

// 3、如果前面两个都相等那就比较服务器id,如果大,则其就是leader

return ((newEpoch > curEpoch)

|| ((newEpoch == curEpoch)

&& ((newZxid > curZxid)

|| ((newZxid == curZxid)

&& (newId > curId)))));

java zookeeper 主从热备_zookeeper 核心原理相关推荐

  1. mysql5.6热升级_Mysql5.6主从热备配置

    数据库是应用系统的核心,为了保证数据库的安全采用主从热备是很常见的方法,也就是主数据库DDL.DML都将被同步到从数据库. 一.      实验环境 操作系统:windowsserver 2008 R ...

  2. haproxy keepalived_企业实战KeepAlived+Redis+Haproxy实现主从热备、负载均衡、秒级切换...

    最近公司生产环境需要做一个Redis+Keepalived+Haproxy的集群架构,分别用六个端口,实现多路复用,最终实现主从热备.负载均衡.秒级切换. 一,Redis6.06集群官网部署,这里不再 ...

  3. 综合项目:Keepalived+Redis+Haproxy实现主从热备、负载均衡、秒级切换

    Keepalived+Redis+Haproxy 群集 一.部署 Redis 群集 1.编译安装 Redis 1)创建群集目录 2)编辑配置文件 3)复制配置文件到每个实例 4)启动 Redis 实例 ...

  4. Windows server 2012 R2 DHCP主从热备配合华为交换机DHCP中继配置详解(非域控版本)

    最近在给部门的Windows DHCP服务器配置主从热备,这里记录下操作过程以及一些坑和解决方法.同时因为涉及到跨网段的DHCP,还需要在三层交换机上配置DHCP中继. 我是T型人小付,一位坚持终身学 ...

  5. mysql innodb热备_mysql主从热备配置(含innodb)终极版

    转自 http://blogread.cn/it/article/361?f=wb mysql主从热备有2种配置方式,备份某些库或者忽略备份某些库,建议选择后者 master机器 以下是代码片段: v ...

  6. njx如何实现负载均衡_LVS + keepalived + nginx + tomcat 实现主从热备 + 负载均衡

    前言 首先声明下,由于这两天找资料,看了不少博客 ,但是出于不细心,参考者的博客地址没有记录下来,所有文中要是出现了与大家博客相同的地方,那么请大家在评论区说明并附上博客地址,我好引用进来:这里表示抱 ...

  7. Mac内CentOS7虚拟机搭建lvs+keepalived+nginx双机主从热备

    Mac内CentOS7虚拟机搭建lvs+keepalived+nginx双机主从热备 [目前列出操作大纲,后续复习完善详细配置内容] MAC:00:50:56:38:3D:80 2核4G,硬盘20G ...

  8. Mysql双机热备配置方案原理及实战

    目录 1 基本概念 2 MySQL热备原理 3 配置步骤 3.2 A(主服务器)配置 3.3 B(从服务器)配置 4 验证 1 基本概念 双机热备特指基于高可用系统中的两台服务器的热备(或高可用),双 ...

  9. mysql 主从热备_windows10本地两个mysql8服务配置主从热备

    1.前期准备 1.两份mysql源码(不要用下载器安装,避免下载器读取已安装msyql服务而不新增安装选项) 2.新增主从的配置文件my.ini 主数据库配置内容(注意basedir后面的双斜线,是为 ...

最新文章

  1. PHP问题 —— Warning: PHP Startup: Unable to load dyna
  2. Verilog功能模块——符号位扩展
  3. Qt学习笔记之QLatin1Char
  4. Android之版本检测和更新
  5. 争分夺秒:阿里实时大数据技术全力助战双11
  6. MySQL 修改数据
  7. uniapp 移动端上传文件_基于 uniapp 实现动态路由和动态 Tabbar
  8. QGIS教程—缓冲区buffer
  9. BigGAN、BiGAN、BigBiGAN简单介绍
  10. @Lookup注解用法
  11. 算法刻意练习-LeetCode实战29-加油站(C++)
  12. 我的世界服务器卡无限刷物品,我的世界怎么刷物品 我的世界无限刷物品教程...
  13. 关于人工智能AI的发展,边缘计算中的AI芯片
  14. iPhone铃声制作软件:iRingg for Mac
  15. 怎么讲计算机e盘设置共享,共享盘怎么设置(电脑如何设置共享盘)
  16. 域渗透非约束委派Spooler
  17. CodeFun-UI 设计稿智能生成前端源代码
  18. 牛客寒假算法基础集训营5 J 炫酷数学
  19. 【愚公系列】2021年11月 攻防世界-进阶题-MISC-025(Miscellaneous-200)
  20. 编程入门:java基础

热门文章

  1. SpringBoot项目在启动时出现了Cannot determine embedded database driver class for database type NONE 错误
  2. php只刷新一次,怎么让php网页指只刷新一次
  3. 源码编译altas mysql_Atlas的安装
  4. python generator object_python - 将生成器对象转换为列表以进行调试
  5. ZigBee网络数据传递流程_Zigbee网络架构解析
  6. mysql5.6配置区分大小写
  7. 解决批处理命令执行完毕后自动关闭cmd窗口
  8. JS的forEach和map方法的区别
  9. CF 468B Two Sets
  10. Linux怎样创建FTP服务器--修改用户默认目录-完美解决 - 费元星