超全zookeeper知识点与实战
第1章 Zookeeper
1.1 概述
Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。
配合其他服务器,
文件系统——存储各种服务器上线信息
通知机制——客户端跟zookeeper打招呼
1.2 特点
1)Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。
2)集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所以Zookeeper适合安装奇数台服务器。 (如果偶数就浪费了一台集群)
3)全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
4)更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。
5)数据更新原子性,一次数据更新要么成功,要么失败。
(封装成一个大的事务,要么整体成功,要么整体失败)
6)实时性,在一定时间范围内,Client能读到最新数据。
(同步数据,速度非常快,因为zookeeper里面的数据非常小)
1.3 数据结构
数据结构
ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个ZNode。每一个ZNode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识。
存储的数据量比较小,存储的内容有限
1.4 应用场景
提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等。
统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别。
例如:IP不容易记住,而域名容易记住。
统一配置管理
1)分布式环境下,配置文件同步非常常见。
(1)一般要求一个集群中,所有节点的配置信息是一致的,比如 Kafka 集群。
(2)对配置文件修改后,希望能够快速同步到各个节点上。
2) 配置管理可交由ZooKeeper实现。
(1)可将配置信息写入ZooKeeper上的一个Znode。
(2)各个客户端服务器监听这个Znode。
(3)一旦Znode中的数据被修改,ZooKeeper将通知各个客户端服务器。
1)分布式环境中,实时掌握每个节点的状态是必要的。
(1)可根据节点实时状态做出一些调整。
2)ZooKeeper可以实现实时监控节点状态变化
(1)可将节点运行状态信息写入ZooKeeper上的一个ZNode。
(2)监听这个ZNode可获取它的实时状态变化。
相当于讲客户端所有的相关信息注册上之后,进行统一的集群管理,以及集群状态的好坏
服务器动态上下线
软负载均衡
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
服务器注册域名,下面有多台服务器,多台服务器有一个接客线程数,zookeeper根据每一个节点上对应的访问数来进行软负载均衡
1.5 下载地址
1)官网首页:
https://zookeeper.apache.org/
2)下载截图
选择相对稳定的老版本
第2章 Zookeeper本地安装
2.1 本地模式安装部署
1)安装前准备
(1)安装Jdk
(2)拷贝Zookeeper安装包到Linux系统下
先启动三台集群
(3)解压到指定目录
[leokadia@hadoop102 software]$ tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/
查看
改名
[leokadia@hadoop102 module]$ mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7
2)配置修改
(1)将/opt/module/zookeeper-3.5.7/conf这个路径下的zoo_sample.cfg修改为zoo.cfg;
[leokadia@hadoop102 conf]$ mv zoo_sample.cfg zoo.cfg
(2)打开zoo.cfg文件,修改dataDir路径:
[leokadia@hadoop102 conf]$ vim zoo.cfg
/tmp存储的是临时数据,到了一个月会被删除掉,也就是到了1个月zookeeper数据都没了,于是需要创建一个目录进行保存,通常创建zkData
想把数据放在自己的框架下,于是在自己的目录下创建目录zkData
(3)在/opt/module/zookeeper-3.5.7/这个目录上创建zkData文件夹
[leokadia@hadoop102 zookeeper-3.5.7]$ mkdir zkData
在配置文件中修改如下内容:
dataDir=/opt/module/zookeeper-3.5.7/zkData
3)操作Zookeeper
(1)启动Zookeeper
[leokadia@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh start
(2)查看进程是否启动
[leokadia@hadoop102 zookeeper-3.5.7]$ jps
(3)查看状态:
[leokadia@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh status!
(4)启动客户端:
[leokadia@hadoop102 zookeeper-3.5.7]$ bin/zkCli.sh
(5)退出客户端:
[zk: localhost:2181(CONNECTED) 0] quit
(6)停止Zookeeper
[leokadia@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh stop
2.2 配置参数解读
Zookeeper中的配置文件zoo.cfg中参数含义解读如下:
1)tickTime =2000:通信心跳数,Zookeeper服务器与客户端心跳时间,单位毫秒
Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)
即客户端与服务端,服务端与服务段2s中发一次通信
2)initLimit =10:LF初始通信时限
集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器连接到Leader的时限。
初始化在10个心跳(20s)还没有建立连接,这个通信就是失败的
3)syncLimit =5:LF同步通信时限
集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。
4)dataDir:数据文件目录+数据持久化路径
主要用于保存Zookeeper中的数据。
注意:默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录。
5)clientPort =2181:客户端连接端口
监听客户端连接的端口。通常情况下不做修改。
第3章 Zookeeper实战(开发重点)
3.1 分布式安装部署
1)集群规划
在hadoop102、hadoop103和hadoop104三个节点上部署Zookeeper。
2)解压安装
(1)解压Zookeeper安装包到/opt/module/目录下
[leokadia@hadoop102 software]$ tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/
之前本地做过了
(2)同步/opt/module/zookeeper-3.5.7目录内容到hadoop103、hadoop104
[leokadia@hadoop102 module]$ xsync zookeeper-3.5.7/
3)配置服务器编号
(1)在/opt/module/zookeeper-3.5.7/这个目录下创建zkData
[leokadia@hadoop102 zookeeper-3.5.7]$ mkdir -p zkData
(2)在/opt/module/zookeeper-3.5.7/zkData目录下创建一个myid的文件
[leokadia@hadoop102 zkData]$ touch myid
添加myid文件,注意一定要在linux里面创建,在notepad++里面很可能乱码
(3)编辑myid文件
[leokadia@hadoop102 zkData]$ vi myid
在文件中添加与server对应的编号:
2
Hadoop102 配置2,hadoop103配置3,hadoop104配置4
(4)拷贝配置好的zookeeper到其他机器上
[leokadia@hadoop102 zkData]$ xsync myid
如果前面没分发,同步/opt/module/zookeeper-3.5.7目录内容到hadoop103、hadoop104,当然也可以等到后面一起分发
[leokadia@hadoop102 module]$ xsync zookeeper-3.5.7/
并分别在hadoop103、hadoop104上修改myid文件中内容为3、4
4)配置zoo.cfg文件
(1)重命名/opt/module/zookeeper-3.5.7/conf这个目录下的zoo_sample.cfg为zoo.cfg
[leokadia@hadoop102 conf]$ mv zoo_sample.cfg zoo.cfg
(2)打开zoo.cfg文件
[leokadia@hadoop102 conf]$ vim zoo.cfg
修改数据存储路径配置(在上文已经做过了)
dataDir=/opt/module/zookeeper-3.5.7/zkData
增加如下配置
#######################cluster##########################
server.2=hadoop102:2888:3888
server.3=hadoop103:2888:3888
server.4=hadoop104:2888:3888
(3)同步zoo.cfg配置文件
[leokadia@hadoop102 conf]$ xsync zoo.cfg
检查是否分发成功
hadoop103修改成功
(4)配置参数解读
server.A=B:C:D。
A是一个数字,表示这个是第几号服务器;
集群模式下配置一个文件myid,这个文件在dataDir目录下,这个文件里面有一个数据就是A的值,Zookeeper启动时读取此文件,拿到里面的数据与zoo.cfg里面的配置信息比较从而判断到底是哪个server。
B是这个服务器的地址;
C是这个服务器Follower与集群中的Leader服务器交换信息的端口;
D是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的端口。
5)集群操作
(1)分别启动Zookeeper
[leokadia@hadoop102 zookeeper-3.5.7]$ bin/zkServer.sh start
[leokadia@hadoop103 zookeeper-3.5.7]$ bin/zkServer.sh start
[leokadia@hadoop104 zookeeper-3.5.7]$ bin/zkServer.sh start
注:如果只启动一台集群查看状态,发现没有启动起来,因为此时是3台服务器,目前只启动一台服务器,没有达到超过半数,就不会选出对应的Leader,对应的集群就没法工作,集群必须要有超过半数以上的服务器是好的,才能正常工作
如何知道是否超过半数了呢?根据配置文件刚刚配置文件中写了有3台服务器
此时我们启动第二台服务器,可以发现hadoop103成为了leader
再查看hadoop102,发现其成为了follower
此时再启动hadoop104,查看状态
发现104为follower
(2)查看状态(见上图)
[leokadia@hadoop102 zookeeper-3.5.7]# bin/zkServer.sh status
JMX enabled by default
Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg
Mode: follower
[leokadia@hadoop103 zookeeper-3.5.7]# bin/zkServer.sh status
JMX enabled by default
Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg
Mode: leader
[leokadia@hadoop104 zookeeper-3.4.5]# bin/zkServer.sh status
JMX enabled by default
Using config: /opt/module/zookeeper-3.5.7/bin/../conf/zoo.cfg
Mode: follower
3.1.2 集群启动停止脚本
由于如果集群较多的话,每一台集群启动和停止都需要输入命令太过于繁琐,故编写脚本统一启动和停止。
1)在 hadoop102 的/home/leokadia/bin 目录下创建脚本
[leokadia@hadoop102 bin]$ vim zk.sh
在脚本中编写如下内容
#!/bin/bash
case $1 in
"start"){for i in hadoop102 hadoop103 hadoop104doecho ---------- zookeeper $i 启动 ------------ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh start"done
};;
"stop"){for i in hadoop102 hadoop103 hadoop104doecho ---------- zookeeper $i 停止 ------------ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh stop"done
};;
"status"){for i in hadoop102 hadoop103 hadoop104doecho ---------- zookeeper $i 状态 ------------ssh $i "/opt/module/zookeeper-3.5.7/bin/zkServer.sh status"done
};;
esac
2)增加脚本执行权限
[leokadia@hadoop102 bin]$ chmod 777 zk.sh
3)Zookeeper 集群启动脚本
先启动一下hadoop集群
[leokadia@hadoop102 bin]$ jpsall
启动zk集群
[leokadia@hadoop102 bin]$ zk.sh start
查看各集群状态:
[leokadia@hadoop102 bin]$ zk.sh status
4)Zookeeper 集群停止脚本
[leokadia@hadoop102 bin]$ zk.sh stop
3.2 客户端命令行操作
命令基本语法 功能描述
help 显示所有操作命令
ls path 使用 ls 命令来查看当前znode的子节点
-w 监听子节点变化
-s 附加次级信息
create 普通创建
-s 含有序列
-e 临时(重启或者超时消失)
get path 获得节点的值
-w 监听节点内容变化
-s 附加次级信息
set 设置节点的具体值
stat 查看节点状态
delete 删除节点
deleteall 递归删除节点
先启动集群
1)启动客户端
[leokadia@hadoop103 zookeeper-3.5.7]$ bin/zkCli.sh
启动之后发现是个本地的客户端,想要把它变成hadoop102或者103这种的客户端,先输入quit退出,再输入
[leokadia@hadoop102 zookeeper-3.5.7]$ bin/zkCli.sh -server hadoop102:2181
2)显示所有操作命令
[zk: localhost:2181(CONNECTED) 1] help
3)查看当前znode中所包含的内容
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
4)查看当前节点详细数据
[zk: localhost:2181(CONNECTED) 1] ls2 /
[zookeeper]
cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
(1)czxid:创建节点的事务 zxid
每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所 有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之 前发生。
(2)ctime:znode 被创建的毫秒数(从 1970 年开始)
(3)mzxid:znode 最后更新的事务 zxid
(4)mtime:znode 最后修改的毫秒数(从 1970 年开始)
(5)pZxid:znode 最后更新的子节点 zxid (存储的是树形结构)
(6)cversion:znode 子节点变化号,znode 子节点修改次数
(7)dataversion:znode 数据变化号
(8)aclVersion:znode 访问控制列表的变化号
(9)ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是 临时节点则是 0。
(10)dataLength:znode 的数据长度
(11)numChildren:znode 子节点数量
1)分别创建2个普通节点(永久节点)还是用的漫威的例子
[zk: hadoop102:2181(CONNECTED) 7] create /hero "ironman"
Created /hero
[zk: hadoop102:2181(CONNECTED) 9] create /hero/Marvel "Hulk"
Created /hero/Marvel
2)获得节点的值
[zk: hadoop102:2181(CONNECTED) 12] get -s /hero
ironman
cZxid = 0x300000006
ctime = Sat Nov 27 08:07:39 CST 2021
mZxid = 0x300000006
mtime = Sat Nov 27 08:07:39 CST 2021
pZxid = 0x300000007
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 7
numChildren = 1
[zk: hadoop102:2181(CONNECTED) 13] get -s /hero/Marvel
Hulk
cZxid = 0x300000007
ctime = Sat Nov 27 08:10:48 CST 2021
mZxid = 0x300000007
mtime = Sat Nov 27 08:10:48 CST 2021
pZxid = 0x300000007
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
3)创建带序号的节点(永久节点 + 带序号)
[zk: hadoop102:2181(CONNECTED) 4] create -s /hero/DC/Batman "Batman"
Created /hero/DC/Batman0000000000
那么带序号和不带序号的节点有什么区别吗?
带序号的节点可以创建相同名称的节点,序号自动加1,不带序号的不能重复创建同名结点。
验证退出客户端,节点是否还是存在
节点没有被删除
如果原来没有序号节点,序号从 0 开始依次递增。如果原节点下已有 2 个节点,则再排 序时从 2 开始,以此类推。
[zk: hadoop102:2181(CONNECTED) 14] create -e /hero/Darkhorse "darkhorse"
Created /hero/Darkhorse
[zk: hadoop102:2181(CONNECTED) 15] create -e -s /hero/Darkhorse "darkhorse"
Created /hero/Darkhorse0000000003
退出后,发现临时节点消失
9)修改节点数据值
[zk: hadoop102:2181(CONNECTED) 3] set -s /hero/DC "DC"
3.2.4 监听器原理
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目 录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数 据的任何改变都能快速的响应到监听了该节点的应用程序。
1、监听原理详解
- 1)首先在要有一个main()线程
- 2)在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
- 3)通过connect线程将注册的监听事件发送给Zookeeper。
- 4)在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
- 5)Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
- 6)listener线程内部调用了process()方法。
2、常见的监听
1)监听节点数据的变化
get path [watch]
2)监听子节点增减的变化
ls path [watch]
10)节点的值变化监听
(1)在hadoop104主机上注册监听/hero节点数据变化
[zk: localhost:2181(CONNECTED) 1] get -w /hero
(2)在hadoop103主机上修改/sanguo节点的数据
[zk: localhost:2181(CONNECTED) 0] set /hero "Avengers"
(3)观察hadoop104主机收到数据变化的监听
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/hero
再次修改数据,hadoop104无变化,监听不到
注意:在hadoop103再多次修改/hero的值,hadoop104上不会再收到监听。因为注册 一次,只能监听一次。想再次监听,需要再次注册。
2)节点的子节点变化监听(路径变化)
(1)在hadoop104主机上注册监听/sanguo节点的子节点变化
[zk: localhost:2181(CONNECTED) 1] ls -w/hero
(2)在hadoop103主机/sanguo节点上创建子节点
[zk: localhost:2181(CONNECTED) 0] create /hero/Disney "Woody"
Created /hero/Disney
(3)观察hadoop104主机收到子节点变化的监听
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/hero
同理,再在hadoop103创建子节点,hadoop104上不会再收到监听。因为注册一次,只能监听一次。想再次监听,需要再次注册。
注意:节点的路径变化,也是注册一次,生效一次。想多次生效,就需要多次注册。
3.2.5 节点删除与查找
12)删除节点
[zk: localhost:2181(CONNECTED) 7] delete /hero/Disney
2)递归删除节点
[zk: localhost:2181(CONNECTED) 15] deleteall/hero
3)查看节点状态
[zk: localhost:2181(CONNECTED) 17] stat /zookeeper
3.3 客户端API应用
3.3.1 IDEA环境搭建
1)创建一个Maven工程
2)添加pom文件
<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>RELEASE</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.8.2</version></dependency><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.5.7</version></dependency>
</dependencies>
3)拷贝log4j.properties文件到项目根目录
需要在项目的src/main/resources目录下,新建一个文件,命名为“log4j.properties”,在文件中填入。
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
3.3.2 创建ZooKeeper客户端
public class zkClient {// 注意:逗号左右不能有空格private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";private int sessionTimeout = 2000;private ZooKeeper zkClient;@Beforepublic void init() throws IOException {zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {}});}}
3.3.3 创建子节点
@Testpublic void create() throws KeeperException, InterruptedException {String nodeCreated = zkClient.create("/leokadia", "sa.avi".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);//创建持久节点}
在hadoop103上验证是否创建对应节点,创建成功
3.3.4 获取子节点并监听节点变化
@Test
public void getChildren() throws Exception {List<String> children = zkClient.getChildren("/", true);for (String child : children) {System.out.println(child);}// 延时阻塞Thread.sleep(Long.MAX_VALUE);
}
成功打印节点
在集群中创建节点,希望在控制台上有变化所以需要加一个延时
但由于这里只注册监听一次,就失效了,故将其移到上面代码中
在控制台创建节点
成功监听
删除节点也能顺利监听
3.3.5 判断Znode是否存在
@Test
public void exist() throws Exception {Stat stat = zkClient.exists("/eclipse", false);System.out.println(stat == null ? "not exist" : "exist");
}
在集群中将其删除
再次运行代码,不存在
3.4 客户端向服务端写数据流程
3.4.1 写流程之写入请求直接发送给Leader节点
写请求给leader,leader将写请求通知follower,follower写完之后回复leader以ack,当超过半数的写完了,就回复客户端写完了,其余没写完的继续写
3.4.2 写流程之写入请求发送给follower节点
follower没有写权限,将写请求送给有权限的Leader,Leader先自己写一份,写完后,传达写命令给其他的Follower节点,让他们写,当超过半数的写完了,Leader就回复最先收到请求的Follower,整个集群数据写完了,Follower回复客户端写完了,其余没写完的小半数Follower继续写
4 监听服务器节点动态上下线案例
1)需求
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。
2)需求分析
- 服务器上线,可以对外提供服务,告诉zookeeper集群可以对外提供服务:
第一台服务器上线,在zookeeper上创建对应的节点servers/server1 hadoop101 80 nodes,在节点上提供的信息为,该节点主机的名称为hadoop101,当前已经连接了80个客户端(即为该节点存储的数据)
第二台服务器上线,在zookeeper上创建对应的节点servers/server2 hadoop102 90 nodes,在节点上提供的信息为,该节点主机的名称为hadoop102,当前已经连接了90个客户端(即为该节点存储的数据)
第三台服务器上线,在zookeeper上创建对应的节点servers/server3 hadoop103 95 nodes,在节点上提供的信息为,该节点主机的名称为hadoop103,当前已经连接了95个客户端(即为该节点存储的数据)
总结:上线服务器就是在zookeeper集群创建服务器节点的操作 - 客户端获取当前在线服务器列表并注册监听,监控节点数据的变化——监听器原理
- 如果服务器2挂掉了(如果服务器2创建的是一个临时节点-e,该节点servers/server2 hadoop102 90 nodes消失)
- 客户端监听servers节点的变化,收到相关信息,知道该节点下有子节点的删除,即服务器2下线的通知。
- 客户端重新再去获取服务器列表,并注册监听,再考虑连接服务器的时候就不考虑连接服务器2,只会考虑连接服务器1,3。
回顾之前API操作,上面服务器的操作是create操作,下面是-w监听操作
3)具体实现
先将集群之前测试用的多于的节点都删除,只留zookeeper节点
(0)先在集群上创建/servers节点
[zk: localhost:2181(CONNECTED) 11] create /servers “servers”
Created /servers
(1)服务器端向Zookeeper注册代码
package com.leokadia.case1;import org.apache.zookeeper.*;import java.io.IOException;public class DistributeServer {private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";private int sessionTimeout = 2000;private ZooKeeper zk;public static void main(String[] args) throws IOException, KeeperException, InterruptedException {DistributeServer server = new DistributeServer();// 1 获取zk连接server.getConnect();// 2 注册服务器到zk集群server.regist(args[0]);// 3 启动业务逻辑(等待)server.business();}private void business() throws InterruptedException {Thread.sleep(Long.MAX_VALUE);}// 2 注册服务器到zk集群,创建对应的路径,在节点上输入对应主机名称private void regist(String hostname) throws KeeperException, InterruptedException {String create = zk.create("/servers/"+hostname, hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);//路径 主机名称 权限,上线就有,没上线就没有,且有对应的编号,因此创建临时带编号节点-e-sSystem.out.println(hostname +" is online") ;}private void getConnect() throws IOException {//连接器连接上对应的zookeeperzk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {}});}
}
(2)客户端代码
package com.leokadia.case1;import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;import java.io.IOException;
import java.util.ArrayList;
import java.util.List;public class DistributeClient {private String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";private int sessionTimeout = 2000;private ZooKeeper zk;public static void main(String[] args) throws IOException, KeeperException, InterruptedException {DistributeClient client = new DistributeClient();// 1 获取zk连接client.getConnect();// 2 监听/servers下面子节点的增加和删除client.getServerList();// 3 业务逻辑(睡觉)client.business();}private void business() throws InterruptedException {Thread.sleep(Long.MAX_VALUE);}private void getServerList() throws KeeperException, InterruptedException {List<String> children = zk.getChildren("/servers", true);ArrayList<String> servers = new ArrayList<>();for (String child : children) {byte[] data = zk.getData("/servers/" + child, false, null);servers.add(new String(data));}// 打印System.out.println(servers);}private void getConnect() throws IOException {zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {try {getServerList();} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}}});}
}
4.4测试
1)在 Linux 命令行上操作增加减少服务器
(1)启动 DistributeClient 客户端
(2)在hadoop102上zk的客户端/servers 目录上创建临时带序号节点
[zk: localhost:2181(CONNECTED) 1] create -e -s /servers/hadoop102 "hadoop102"
[zk: localhost:2181(CONNECTED) 2] create -e -s /servers/hadoop103 "hadoop103"
(3)观察 Idea 控制台变化 [hadoop102, hadoop103]
(4)执行删除操作
[zk: localhost:2181(CONNECTED) 8] delete /servers/hadoop1020000000000
(5)观察 Idea 控制台变化 [hadoop103]
2)在 Idea 上操作增加减少服务器
(1)启动 DistributeClient 客户端(如果已经启动过,不需要重启)
(2)启动 DistributeServer 服务端
①点击 Edit Configurations…
②在弹出的窗口中(Program arguments)输入想启动的主机,例如,hadoop102
运行server
查看客户端,检测到刚刚下线的hadoop102上线了
更换参数hadoop104
客户端检测
5. 分布式锁的概念
5.1 分布式锁概念
分布式锁:
"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。
1)接收到请求后,在/locks节点下创建一个临时顺序节点
客户端访问集群,创建临时带序号的节点,有n多个客户端,在这个目录下创建自己的节点
2)判断自己是不是当前节点下最小的节点:是,获取到锁;不是,对前一个节点进行监听
3)获取到锁,处理完业务后,delete节点释放锁,然后下面的节点将收到通知,重复第二步判断
客户端在目录下创建临时带序号的节点,序号小的节点优先拿到锁进行相关业务的操作,其他节点如果发现自己不是序号最小的节点就监听前一个节点,前一个节点如果释放,则立即获得这把锁
5.2 原生 Zookeeper 实现分布式锁案例
1)分布式锁实现
package com.leokadia.case2;import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;public class DistributedLock {private final String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";private final int sessionTimeout = 2000;private final ZooKeeper zk;private CountDownLatch connectLatch = new CountDownLatch(1);private CountDownLatch waitLatch = new CountDownLatch(1);private String waitPath;private String currentMode;public DistributedLock() throws IOException, InterruptedException, KeeperException {// 获取连接zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {@Overridepublic void process(WatchedEvent watchedEvent) {// connectLatch 如果连接上zk 可以释放if (watchedEvent.getState() == Event.KeeperState.SyncConnected){connectLatch.countDown();}// waitLatch 需要释放if (watchedEvent.getType()== Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){waitLatch.countDown();}}});// 等待zk正常连接后,往下走程序connectLatch.await();// 判断根节点/locks是否存在Stat stat = zk.exists("/locks", false);if (stat == null) {// 创建一下根节点zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);}}// 对zk加锁 判断是否是序号最小public void zklock() {// 创建对应的临时带序号节点try {currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);// wait一小会, 让结果更清晰一些Thread.sleep(10);// 判断创建的节点是否是最小的序号节点,如果是获取到锁;如果不是,监听他序号前一个节点List<String> children = zk.getChildren("/locks", false);// 如果children 只有一个值,那就直接获取锁; 如果有多个节点,需要判断,谁最小if (children.size() == 1) {return;} else {Collections.sort(children);// 获取节点名称 seq-00000000String thisNode = currentMode.substring("/locks/".length());// 通过seq-00000000获取该节点在children集合的位置int index = children.indexOf(thisNode);// 判断if (index == -1) {System.out.println("数据异常");} else if (index == 0) {// 就一个节点,可以获取锁了return;} else {// 需要监听 他前一个节点变化waitPath = "/locks/" + children.get(index - 1);zk.getData(waitPath,true,new Stat());// 等待监听waitLatch.await();return;}}} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}}// 解锁public void unZkLock() {// 删除节点try {zk.delete(this.currentMode,-1);} catch (InterruptedException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();}}
}
2)分布式锁测试
(1)创建两个线程
package com.leokadia.case2;import org.apache.zookeeper.KeeperException;import java.io.IOException;public class DistributedLockTest {public static void main(String[] args) throws InterruptedException, IOException, KeeperException {final DistributedLock lock1 = new DistributedLock();final DistributedLock lock2 = new DistributedLock();new Thread(new Runnable() {@Overridepublic void run() {try {lock1.zklock();System.out.println("线程1 启动,获取到锁");Thread.sleep(5 * 1000);lock1.unZkLock();System.out.println("线程1 释放锁");} catch (InterruptedException e) {e.printStackTrace();}}}).start();new Thread(new Runnable() {@Overridepublic void run() {try {lock2.zklock();System.out.println("线程2 启动,获取到锁");Thread.sleep(5 * 1000);lock2.unZkLock();System.out.println("线程2 释放锁");} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}
(2)观察控制台变化:
也有可能线程2先获取到锁
5.3 Curator 框架实现分布式锁案例
1)原生的 Java API 开发存在的问题
(1)会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
(2)Watch 需要重复注册,不然就不能生效
(3)开发的复杂性还是比较高的
(4)不支持多节点删除和创建。需要自己去递归
2)Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。 详情请查看官方文档:https://curator.apache.org/index.html
3)Curator 案例实操
(1)添加依赖
<dependency><groupId>org.apache.curator</groupId><artifactId>curator-framework</artifactId><version>4.3.0</version></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-recipes</artifactId><version>4.3.0</version></dependency><dependency><groupId>org.apache.curator</groupId><artifactId>curator-client</artifactId><version>4.3.0</version></dependency>
(2)代码实现
package com.leokadia.case3;import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;public class CuratorLockTest {public static void main(String[] args) {// 创建分布式锁1InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");// 创建分布式锁2InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");new Thread(new Runnable() {@Overridepublic void run() {try {lock1.acquire();System.out.println("线程1 获取到锁");lock1.acquire();System.out.println("线程1 再次获取到锁");Thread.sleep(5 * 1000);lock1.release();System.out.println("线程1 释放锁");lock1.release();System.out.println("线程1 再次释放锁");} catch (Exception e) {e.printStackTrace();}}}).start();new Thread(new Runnable() {@Overridepublic void run() {try {lock2.acquire();System.out.println("线程2 获取到锁");lock2.acquire();System.out.println("线程2 再次获取到锁");Thread.sleep(5 * 1000);lock2.release();System.out.println("线程2 释放锁");lock2.release();System.out.println("线程2 再次释放锁");} catch (Exception e) {e.printStackTrace();}}}).start();}private static CuratorFramework getCuratorFramework() {ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);CuratorFramework client = CuratorFrameworkFactory.builder().connectString("hadoop102:2181,hadoop103:2181,hadoop104:2181").connectionTimeoutMs(2000).sessionTimeoutMs(2000).retryPolicy(policy).build();// 启动客户端client.start();System.out.println("zookeeper 启动成功");return client;}
}
(2)观察控制台变化:
线程 1 获取锁
线程 1 再次获取锁
线程 1 释放锁
线程 1 再次释放锁
线程 2 获取锁
线程 2 再次获取锁
线程 2 释放锁
线程 2 再次释放锁
第4章 Zookeeper内部原理
这一部分对上述曾经提到过的内部原理进行总结,并阐述Zookeeper的一些算法基础
6.1 节点类型
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
(1)持久化目录节点:客户端与Zookeeper断开连接后,该节点依旧存在
(2)持久化顺序编号目录节点:客户端与Zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
(3)临时目录节点:客户端与Zookeeper断开连接后,该节点被删除
(4)临时顺序编号目录节点:客户端与 Zookeeper 断开连接后,该节点被删除,只是 Zookeeper给该节点名称进行顺序编号。
说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
6.2 Stat结构体(节点的详细数据)
(1)czxid-创建节点的事务zxid
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。
事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
(2)ctime - znode被创建的毫秒数(从1970年开始)
(3)mzxid - znode最后更新的事务zxid
(4)mtime - znode最后修改的毫秒数(从1970年开始)
(5)pZxid-znode最后更新的子节点zxid
(6)cversion - znode子节点变化号,znode子节点修改次数
(7)dataversion - znode数据变化号
(8)aclVersion - znode访问控制列表的变化号
(9)ephemeralOwner- 如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0。
(10)dataLength- znode的数据长度
(11)numChildren - znode子节点数量
6.3 监听器原理
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目 录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数 据的任何改变都能快速的响应到监听了该节点的应用程序。
1、监听原理详解
1)首先在要有一个main()线程
2)在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
3)通过connect线程将注册的监听事件发送给Zookeeper。
4)在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
5)Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
6)listener线程内部调用了process()方法。
2、常见的监听
1)监听节点数据的变化
get path [watch]
2)监听子节点增减的变化
ls path [watch]
6.4 选举机制
(1)半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器。
(2)Zookeeper虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper工作时,是有一个节点为Leader,其他则为Follower,Leader是通过内部的选举机制临时产生的。
(3)以一个简单的例子来说明整个选举的过程。
假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么。
Zookeeper的选举机制
6.4.1 第一次启动
(1)服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING;
(2)服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING
(3)服务器3启动,发起一次选举。先投自己一票,此时服务器1和2发现服务器3的myid比自己目前投票推举的(服务器2)大,都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
(4)服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;
(5)服务器5启动,同4一样当小弟。
总结:先选自己,选完之后选票不够,就会把选票投给myid大的节点
一旦选举成功后,其他节点自动变成follower状态,再次启动的节点
相关概念:
SID:服务器ID。用来唯一标识一台 ZooKeeper集群中的机器,每台机器不能重复,和myid一致。
ZXID:事务ID。ZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻, 集群中的每台机器的ZXID值不一定完全一致,这和ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。
Epoch:每个Leader任期的代号。没有Leader时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加
6.4.2 非第一次启动
(1)当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举:
• 服务器初始化启动。
• 服务器运行期间无法和Leader保持连接。
(2)而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态:
• 集群中本来就已经存在一个Leader。
对于第一种已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连接,并进行状态同步即可。
• 集群中确实不存在Leader。
假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻, 3和5服务器出现故障,因此开始进行Leader选举。
选举Leader规则: ①EPOCH大的直接胜出 ②EPOCH相同,事务id大的胜出 ③事务id相同,服务器id大的胜出
6.5 写数据流程
6.5.1 写流程之写入请求直接发送给Leader节点
写请求给leader,leader将写请求通知follower,follower写完之后回复leader以ack,当超过半数的写完了,就回复客户端写完了,其余没写完的继续写
6.5.2 写流程之写入请求发送给follower节点
follower没有写权限,将写请求送给有权限的Leader,Leader先自己写一份,写完后,传达写命令给其他的Follower节点,让他们写,当超过半数的写完了,就回复客户端写完了,其余没写完的继续写
6.6 拜占庭将军问题
拜占庭将军问题是一个协议问题,拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的,并且将 军中存在叛徒。叛徒可以任意行动以达到以下目标:欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,如当将军们不希望进攻时促成进攻 行动;或者迷惑某些将军,使他们无法做出决定。如果叛徒达到了这些目的之一,则任何攻击行动的结果都是注定要失败的,只有完全达成一致的努力才能获得胜利。
这个问题实际上就是多台服务器处理某一个一致性问题,解决分布式一致性的经典算法之一是下文中的Paxos算法。
6.4 Paxos算法
Paxos算法是一种基于消息传递且具有高度容错特性的一致性算法。
分布式系统中的节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing)。基于消息传递通信模型的分布式系统,不可避免的会发生以下错误:进程可能会慢、被杀死或者重启,消息可能会延迟、丢失、重复,在基础 Paxos 场景中,先不考虑可能出现消息篡改即拜占庭错误的情况。Paxos 算法解决的问题是在一个可能发生上述异常的分布式系统中如何就某个值达成一致,保证不论发生以上任何异常,都不会破坏决议的一致性。
Paxos算法描述:
• 在一个Paxos系统中,首先将所有节点划分为Proposer(提议者),Acceptor(接受者),和 Learner(学习者)。(注意:每个节点都可以身兼数职)。
一个完整的Paxos算法流程分为三个阶段:
• Prepare准备阶段
• Proposer向多个Acceptor发出Propose请求Promise(承诺)
• Acceptor针对收到的Propose请求进行Promise(承诺)
• Accept接受阶段
• Proposer收到多数Acceptor承诺的Promise后,向Acceptor发出Propose请求
• Acceptor针对收到的Propose请求进行Accept处理
• Learn学习阶段:Proposer将形成的决议发送给所有Learners
Paxos算法流程中的每条消息描述如下:
(1)Prepare: Proposer生成全局唯一且递增的Proposal ID (可使用时间戳加Server ID),向所有Acceptors发送Prepare请求,这里无需携带提案内容,只携带Proposal ID即可。
(2)Promise: Acceptors收到Prepare请求后,做出“两个承诺,一个应答”。
两个承诺:
不再接受Proposal ID小于等于(注意:这里是<= )当前请求的Prepare请求。
不再接受Proposal ID小于(注意:这里是< )当前请求的Propose请求。
一个应答:
不违背以前做出的承诺下,回复已经Accept过的提案中Proposal ID最大的那个提案的Value和Proposal ID,没有则返回空值。
(3)Propose: Proposer 收到多数Acceptors的Promise应答后,从应答中选择Proposal ID最大的提案的Value,作为本次要发起的提案。如果所有应答的提案Value均为空值,则可以自己随意决定提案Value。然后携带当前Proposal ID,向所有Acceptors发送Propose请求。
(4)Accept: Acceptor收到Propose请求后,在不违背自己之前做出的承诺下,接受并持久化当前Proposal ID和提案Value。
(5)Learn: Proposer收到多数Acceptors的Accept后,决议形成,将形成的决议发送给所有Learners。
下面我们针对上述描述做三种情况的推演举例:为了简化流程,我们这里不设置Learner。
Paxos算法缺陷:在网络复杂的情况下,一个应用Paxos算法的分布式系统,可能很久无法收敛,甚至陷入活锁的情况。造成这种情况的原因是系统中有一个以上的Proposer,多个Proposers相互争夺Acceptors,造成迟迟无法达成一致的情况。针对这种情况,一种改进的Paxos算法被提出:从系统中选出一个节点作为Leader,只有Leader能够发起提案。这样,一次Paxos流程中只有一个Proposer,不会出现活锁的情况,此时只会出现例子中第一种情况。
超全zookeeper知识点与实战相关推荐
- Lumen超全功能知识点来了,ue5初学者们必看
Lumen 是Unreal Engine 5的全动态全局照明和反射系统,开箱即用.它专为建筑可视化等游戏之外的下一代游戏机和高端可视化而设计.在这里,我们将介绍 Lumen 的功能并概述技术细节. 全 ...
- 视频教程-JavaWeb从入门到实战,超全知识点+仿京东项目-Java
JavaWeb从入门到实战,超全知识点+仿京东项目 12年以上开发经验,担任亚信科技核心架构师,丰富的JavaEE开发和Oracle数据库设计经验,精通Java\JavaScript\HTML5等编程 ...
- 2017二级c语言成绩,2017全国计机等级考试二级C语言知识点超全整(打印版).docx
2017全国计机等级考试二级C语言知识点超全整(打印版) 全国计算机 等级考试 目录 第一部分 公共基础知识 第 1 章 算法与数据结构1 HYPERLINK \l "_TOC_250068 ...
- 腾讯、阿里、百度高工都点头称赞的“Redis 实战超全笔记”,不看你就亏大发了
写在开头 如何系统,全面,的学习redis呢? 我的一个程序员朋友,在之前有面试 Java 开发工程师岗位时,居然大部分的面试问题都是关于 Redis 的,他都差点都忘记了自己应聘的是 Java 工程 ...
- linux集群课程,超全Linux集群技术高俊峰-实战式教学视频课程(43讲全)
超全Linux集群技术高俊峰-实战式教学视频课程(43讲全) 1.jpg (63.6 KB, 下载次数: 0) 2016-4-27 11:41 上传 1.jpg (38.51 KB, 下载次数: 0) ...
- Pandas知识点超全总结
Pandas知识点超全总结 一.数据结构 1.Series 1.创建 2.切片.修改 3.其他属性 2.DataFrame 1.创建 2.切片 3.增加.修改 4.删除 5.查看 二.读写数据 1.读 ...
- java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)
目录 前言 1. 学习路线 2. 学习方法 前言 这篇文章主要总结我之前所学过的框架以及学习路线 从实打实的零基础到框架再到项目 之后实习工作 也在这篇博客中记录我的学习笔记 以及在笔记中遇到的配置安 ...
- 2021年超全超详细的最新大数据开发面试题,附答案解析
版本 时间 描述 V1.0 2020-12-18 创建 V1.2 2021-01-17 新增 spark 面试题 V1.3 2021-01-18 新增 kafka 面试题 V1.4 2021-01-2 ...
- 免费的计算机编程类中文书籍(超全)
https://github.com/justjavac/free-programming-books-zh_CN 语言无关类 操作系统 智能系统 分布式系统 编译原理 函数式概念 计算机图形学 WE ...
最新文章
- 动态环境下的SLAM:DynaSLAM 论文学习笔记
- 从语句 char* p=test 说起
- 大数据为何让传统银行焦虑?
- 大学毕业后八大让你加班成狗专业盘点,你的专业上榜了嘛?
- 问题 G: 果汁店的难题(贪心)
- cocos label html文本,【cocos2dx】创建简单的文字Label——BMFont
- Axure函数与变量
- 《音乐达人秀:Adobe Audition实战200例》——实例7 定时录制网络音乐节目
- 常用公有云接入——华为
- java嵌入式开发neo4j_java-嵌入式Neo4j实际如何工作?
- vue中$nextTick()作用
- 子查询三(在FROM子句中使用子查询)
- Java设计模式(四)——再谈观察者模式
- 无法启动此程序 因为计算机中丢失 msvcp71.dll,msvcp71.dll丢失怎样修复?计算机中丢失msvcp71.dll文件的解决方法...
- 为何公众号推送会延迟发送_微信公众号客服消息群发和推送功能如何实现?
- dpdk LRO功能总结
- 黑客教父郭盛华:11个IDA Pro反汇编程序的替代品
- Simulink 环境基础知识(二十一)--优化、估计和扫描模块参数值
- 视频教程-Unity5入门及进阶项目实战 星际迷航-Unity3D
- 一个简洁的倒计时shell 脚本
热门文章
- 为改善Siri功能Apple收购语音助理初创公司
- javaweb总——注:跟着狂神学的
- javaweb学习笔记(六)
- Bounding box regression RCNN我的理解
- 使用JavaScript 中的Math对象和勾股定理公式,计算鼠标的位置与页面图片中心点的距离,根据距离对页面上的图片进行放大或缩小处理。距离远时图片放大,距离近时图片缩小
- web数据管理 期末
- 877E - Danil and a Part-time Job
- GSMA MWC19巴塞罗那圆满落下帷幕
- 开源App动画Lottie
- java lambda 画蛇添足_技术史上的画蛇添足: Redis HGETALL 排序问题