一. 安装:
(1) docker-compose.yml
version: "3.1"
services:
zk:
image: daocloud.io/daocloud/zookeeper:latest
restart: always
container_name: zk
ports:
- 2181:2181
(2) 进入zk容器内部,进入bin目录执行下面的zk客户端命令
docker ps -> docker exec -it 容器id bash
ls -> cd bin/ -> ls -> ./zkCli.sh
二. 架构: 节点Znode -> 存数据 -> 节点不能重复
三. Znode类型:
1.节点类型: - 持久节点:永久的保存在你的Zookeeper - 持久有序节点:永久的保存在你的Zookeeper,他会给节点添加一个有序的序号, /xx -> /xx0000001 - 临时节点:当存储的客户端和Zookeeper服务断开连接时,这个临时节点会自动删除 - 临时有序节点:当存储的客户端和Zookeeper服务断开连接时,这个临时节点会自动删除,他会给节点添加一个有序的序号,/xx -> /xx0000001 2. 监听通知机制
四. 监听机制:
public class Test03 { CuratorFramework cf = ZkUtil.cf(); @Test public void listen() throws Exception { //1. 创建NodeCache对象,指定要监听的znode final NodeCache nodeCache = new NodeCache(cf,"/qf"); nodeCache.start(); //2. 添加一个监听器 nodeCache.getListenable().addListener(new NodeCacheListener() { @Override public void nodeChanged() throws Exception { byte[] data = nodeCache.getCurrentData().getData(); Stat stat = nodeCache.getCurrentData().getStat(); String path = nodeCache.getCurrentData().getPath(); System.out.println("监听的节点是:" + path); System.out.println("节点现在的数据是:" + new String(data,"UTF-8")); System.out.println("节点状态是:" + stat); } }); System.out.println("开始监听!!"); //3. System.in.read();//让监听不退出 System.in.read(); } }
五. zkCli.sh常用命令:
# 查询当前节点下的全部子节点 ls 节点名称 # 例子 ls / # 查询当前节点下的数据 get 节点名称 # 例子 get /zookeeper # 创建节点 create [-s] [-e] znode名称 znode数据 # -s:sequence,有序节点 # -e:ephemeral,临时节点 # 修改节点值 set znode名称 新数据 # 删除节点 delete znode名称 # 删除没有子节点的znode rmr znode名称 # 删除当前节点和全部的子节点(如果rmr不行就用deleteall)
六. 集群:
1. 主从之分 2. 主节点: master -> 执行读写 3. 从节点: slave -> 执行读 4. 角色: (1). Leader: 主节点 (2). Follower: 选举新的leader (3). Observer: 不参与投票 (4). Looking: 正在寻找leader节点 5. 投票策略: (1) Zxix 数字越大, 数据越新 (2) myid: 每个zookeeper服务分配一个全局Uid (3) zookeeper写数据时, 每个节点的FIFO队列, 顺序不会乱, 数据越新zxid越大 (4) zxid相同节点中, 选择myid最大的节点
5. 集群搭建-Linux服务器:
(1) yml文件 version: "3.1" services: zk1: image: zookeeper restart: always container_name: zk1 ports: - 2181:2181 environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1=zk1:2888:3888;2181 (2) # exit退出容器↓ root@b5e3889b3e91:/opt/zookeeper/bin# exit (3) # 避免端口冲突,停止并删除单机版zk然后修改yml文件为集群配置,然后重启容器 docker-compose down docker-compose up -d (4) # 分别进入各自容器内部的bin目录,执行./zkServer.sh status查看状态信息,看看谁是leader ./zkServer.sh status
七. Java操作Zookeeper:
(1) 导入依赖: <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.6.0</version> </dependency> //CuratorFrameworkFactory工具类的依赖 <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.0.1</version> </dependency> (2) 连接Zookeeper集群工具类 public class ZkUtil { public static CuratorFramework cf(){ RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,2);//需重试策略,每隔3秒,重试2次 CuratorFramework cf = CuratorFrameworkFactory.builder() .connectString("10.20.159.25:2181,10.20.159.25:2182,10.20.159.25:2183") .retryPolicy(retryPolicy) .build(); cf.start(); return cf; } } (3) 调用工具类,运行单元测试,如果没有报错,说明连接zk成功 public class Test01 { @Test public void testConnectToZk() { CuratorFramework cf = ZkUtil.cf(); System.out.println(cf); } }
2. Java操作节点:
@Test public void create0() throws Exception { cf.create().withMode(CreateMode.PERSISTENT).forPath("/qf","qfsj".getBytes()); } //获取数据,指定路径,哪个节点的↓ @Test public void getData() throws Exception { byte[] bytes = cf.getData().forPath("/qf"); System.out.println(new String(bytes,"UTF-8"));//qfsj } //创建节点并伴随创建节点的类型,指定路径包装节点和数据↓ @Test public void create() throws Exception { cf.create().withMode(CreateMode.PERSISTENT).forPath("/qf2","uuuu".getBytes()); } //设置数据,指定路径,哪个节点,和修改数据↓ @Test public void update() throws Exception { cf.setData().forPath("/qf2","oooo".getBytes()); } //删除节点,如果有子孩子也随便删除,指定路径,哪个节点↓ @Test public void delete() throws Exception { cf.delete().deletingChildrenIfNeeded().forPath("/qf2"); } //检查存在节点状态,指定路径,哪个节点↓ @Test public void stat() throws Exception { Stat stat = cf.checkExists().forPath("/qf"); System.out.println(stat); } }
八. 过半数存活原则:
1. 存活机器数量超过总数量一半时, 集群才能正常工作, 否则拒绝
2. 2N+1: 奇数台
3. 作用: 解决脑裂问题-> 集群监听不到leader心跳-> 集群判断leader出错-> 集群分裂-> 各自选举各自leader节点-> 集群中出现多个leader-> 无法保证数据一致性;
九. 应用场景:
1. 配置文件管理-> 配置中心-> 保存到配置到目录节点-> 应用监听节点-> 配置发生变化->Zookeeper通知应用-> 程序更新配置信息
2. 集群管理: 所有机器约定在父目录GroupMembers创建目录节点-> 监听父目录子节点-> 机器挂掉-> 机器与zookeeper连接断开-> 机器的临时目录节点被删除-> 其他机器收到通知
3. 分布式锁: zookeeper的节点作为锁-> 所有客户端创建/distribute-lock节点-> 节点创建成功的客户端获得锁-> 用户删除创建的节点释放锁
(1) 分布式锁: 临时有序节点-> 客户端在指定目录下创建临时有序节点-> 序号最小的客户端获得锁-> 如当前节点序号不是最小的-> 监听比自己小一号的节点-> 监听的节点被删除后-> 当前节点再次判断自己是否是最小节点-> 获取锁资源 (2) 并发工具测试: ab压力测试工具 (3) 依赖: zookeeper / curator-framework (4) ZKUtil.java配置类 @Configuration public class ZkConfig { @Bean public CuratorFramework cf(){ RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000,2);//需要指定重试间隔和重试次数 CuratorFramework curatorFramework = CuratorFrameworkFactory.builder() .connectString("10.20.159.25:2181,10.20.159.25:2182,10.20.159.25:2183") .retryPolicy(retryPolicy) .build(); curatorFramework.start(); return curatorFramework; } } (3) 业务代码加锁: InterProcessMutex lock = new InterProcessMutex(cf,"/lock"); //加锁 lock.acquire(); lock.acquire(1,TimeUnit.SECONDS); //指定排队多久放弃获取锁资源 //----------------业务逻辑代码------------------------ //释放锁 lock.release(); (4) 原理图解: 请求一个个来-> Znode临时有序节点-> 最小序号的节点来获取锁
4. 命名服务(Dubbo监控中心): 服务A访问服务B-> B服务未完成-> 服务B部署到Zookeeper-> 添加服务B的节点和数据-> 服务A监控zookeeper的B节点-> 发现服务B有数据后访问B服务
5. 发现服务: 注册持久节点Service/Business-> 每个子节点(临时挂掉自动移除)都是可用服务-> 保存服务的端口-> 其他服务调用zookeeper的节点信息
十. 拓展:使用Redis实现分布式锁 -> 比较轻量级(zk重量级锁, 创建很多节点)
1. 原理: 多线程请求一个个来-> 存储键值对到redis-> 键作为标记-> 键不存在则获取锁-> 存在则不获取-> Redis单线程setnxex-> 处理完删除键释放锁
2. 死锁: 设置键的过期时间 expire(zookeeper自动断开)
3. 代码实现:
(1) 依赖: spring-boot-starter-data-redis (2) yml配置: spring: redis: host: localhost/ port: 6379 (3) RedisLockUtil.java工具类 @Component public class RedisLockUtil { @Autowired private StringRedisTemplate redisTemplate; public boolean lock(String key,String value,int second){ return redisTemplate.opsForValue().setIfAbsent(key,value,second, TimeUnit.SECONDS); } public void unlock(String key){ redisTemplate.delete(key); } } (4) 加锁-> 业务逻辑放入固定代码 @Autowired private RedisLockUtil lock; @GetMapping("/redis/kill") public String redisKill(String item) throws Exception { //加锁 if(lock.lock(item,System.currentTimeMillis() + "",1)){ //业务代码... //释放锁 lock.unlock(item); } }