zookeeper

本文深入探讨Zookeeper,从概述、工作机制、特点到数据结构和应用场景。讲解了选举机制、节点类型和监听器原理,以及如何在开发中进行客户端操作和API应用。通过模拟美团商家上下线和分布式锁案例,展示了Zookeeper在实际项目中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

分布式技术-Zookeeper

1. Zookeeper概述

1.1 概述

  • Zookeeper是一个开源的分布式(多台服务器干一件事)的,为分布式应用提供协调服务的 Apache项目。

  • 美团,饿了么,淘宝,58同城等等应用都是zookeeper的现实生活版。

  • 在大数据技术生态圈中,zookeeper(动物管理员),Hadoop(大象),Hive(蜜蜂), Pig(猪)等技术

1.2 工作机制

  • Zookeeper从设计模式角度来理解:是一个基于观察者模式(一个人干活,有人盯着他)设计的分布式服务管理框架

  • 它负责 存储 和 管理 大家都关心的数据。

    • 然后接受观察者的注册,一旦这些数据的发生变化
    • Zookeeper就将负责通知已经注册的那些观察者做出相应的反应
    • 从而实现集群中类似Master/Slave管理模式
  • Zookeeper = 文件系统 + 通知机制

  1. 商家营业并入驻(连接到zookeeper,注册到集群)
  2. 获取到当前营业的饭店列表(获取服务器列表信息)
  3. 服务器节点下线–>触发通知
  4. 重新再去获取服务器列表,并注册监听。(监听事件再次获取列表)

1.3 特点

1.3.1 分布式和集群的区别?
  • 相同点:无论分布式和集群,都是很多人在做事情。
  • 不同点:集群所做的事情是一样分布式负责的事情不同,但最终也是为总项目工作。

  1. 一个leader和多个follower来组成的集群(狮群中,一头雄狮,N头母狮)

  2. 集群中只要有半数以上的节点存活,Zookeeper就能正常工作(5台服务器挂2台,没问题;4台服务器挂2台,就停止)。适合奇数台服务器

  3. 全局数据一致性,每台服务器都保存一份相同的数据副本,无论client连接哪台server,数据都是一致的。

  4. 数据更新原子性,一次数据要么成功,要么失败(不成功便成仁)

  5. 实时性,在一定时间范围内,client能读取到最新数据

  6. 更新的请求按照顺序执行,会按照发送过来的顺序,逐一执行(发来123,执行123,而不是321或者别的)

1.4 数据结构

  • ZooKeeper数据模型的结构与linux文件系统很类似,整体上可以看作是一棵树,每个节点称做一 个ZNode(ZookeeperNode)。

  • 每一个ZNode默认能够存储1MB的数据(元数据),每个ZNode的路径都是唯一的

    • 元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、 资源查找、文件记录等功能

1.5 应用场景

  • 提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等
1.5 .1 统一命名服务
  • 在分布式环境下,通常需要对应用或服务进行统一的命名,便于识别。

1.5.2 统一配置管理

  • 将配置管理交给Zookeeper。做到修改 一处就快速同步到每台服务器上
    1. 将配置信息写入到Zookeeper的某个节点上。
    2. 每个客户端都监听这个节点。
    3. 一旦节点中的数据文件被修改,Zookeeper这个话匣子就会通知每台客户端服务器。
1.5. 3 服务器节点动态上下线
  • 客户端能实时获取服务器上下线的变化。

1.5.4 软负载均衡
  • Zookeeper会记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户请求(雨露均沾)

1.6 配置参数解读

  • tickTime =2000:通信心跳数,Zookeeper服务器与客户端心跳时间,单位毫秒。
    • Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就 是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
  • initLimit=10:LF初始通信时限
    • 集群中的Follower跟随者服务器与Leader领导者服务器之间,启动时能容忍的最多心跳数。
    • 10*2000(10个心跳时间)如果领导和跟随者没有发出心跳通信,就视为失效的连接,领导和跟随者彻底断开
  • syncLimit =5:LF同步通信时限。
    • 集群启动后,Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime->10秒,Leader就认为Follwer已经死掉,会将Follwer从服务器列表中删除

  • clientPort =2181:客户端连接端口

  • dataDir:数据文件目录+数据持久化路径(保存Zookeeper中的数据)

  • dataLogDir:日志文件目录

2. Zookeeper内部原理

2.1 选举机制(面试重点)

  • 虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper工作时,是有一个节点为 Leader,其他则为Follower,Leader是通过内部的选举机制临时产生的。

  1. Server1先投票,投给自己,自己为1票,没有超过半数,根本无法成为leader,顺水推舟将票数投给了id比自己大的Server2
  • Server2也同上。
  • Server3得到了Server1和Server2的两票,再加上自己投给自己的一票。3票超过半数,顺利成为 leader。
  • Server4和Server5都投给自己,但是无法改变Server3的票数,只好听天由命,承认Server3是 leader。

2.2 节点类型

  • 持久型(persistent):
    • 持久化目录节点(persistent):客户端与zookeeper断开连接后,该节点依旧存在。
    • 持久化顺序编号目录节点(persistent_sequential):创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护,例如:Znode001,Znode002
  • 短暂型(ephemeral):断开连接后节点删除
    • 临时目录节点(ephemeral)
    • 临时顺序编号目录节点(ephemeral_sequential)。

2.3 监听器原理(面试重点)

  1. 在main方法中创建Zookeeper客户端的同时就会创建两个线程一个负责网络连接通信,一个负责监听
  2. .监听事件就会通过网络通信发送给zookeeper(发送监听)。
  3. zookeeper获得注册的监听事件后,立刻将监听事件添加到监听列表里。
  4. zookeeper监听到** 数据变化** 或 路径变化,就会将这个消息发送给监听线程
  • 常见的监听:
    • 监听节点数据的变化:get path [watch]
    • 监听子节点增减的变化:ls path [watch]
  1. 监听线程就会在内部调用process方法。

2.4 写数据流程

  1. Client 想向 ZooKeeper 的 Server1 上写数据,必须的先发送一个写的请求
  2. 如果Server1不是Leader,那么Server1 会把接收到的请求进一步转发给Leader
  3. 这个Leader 会将写请求广播给各个Server,各个Server写成功后就会通知Leader
  4. Leader收到半数以上的 Server 数据写成功了,那么就说明数据写成功了。
  5. 随后,Leader会告诉Server1数据写成功了。
  6. ** Server1会反馈通知 Client** 数据写成功了,整个流程结束。

请求–>转发给leader–>leader广播–>server开始写入,通知leader–>leader收到半数成功通知,写入成功–>leader告诉server,后者反馈至client。

3. Zookeeper实战(开发重点)

3.1 客户端命令行操作

  • 启动服务
    [root@localhost bin]# ./zkServer.sh start

  • 查看状态
    [root@localhost bin]# ./zkServer.sh status

  • 启动客户端

[root@localhost bin]# ./zkCli.sh
  • 显示所有操作命令
help
  • 查看当前znode中所包含的内容
ls /# 根节点下
  • 查看当前节点详细数据
ls -s /
  • cversion:创建版本号,子节点修改次数

  • dataVersion:数据变化版本号

  • 创建节点

create /china
create /usa
  • 创建包含数据的节点
create /ru "pujing"
  • 多级创建节点
create /japan/Tokyo "hot"
# japan必须提前创建好,否则报错 “节点不存在”
  • 获得节点的值
get /japan/Tokyo
  • 创建短暂节点:创建成功之后,quit退出客户端,重新连接,短暂的节点消失
create -e /uk
ls /
quit
ls /
  • 创建带序号的节点
# 在俄罗斯ru下,创建3个city
create -s /ru/city # 执行三次
ls /ru
[city0000000000, city0000000001, city0000000002]
  • 修改节点数据值
set /japan/Tokyo "too hot"
  • 监听 节点的值变化 或 子节点变化(路径变化)

    1. 在server3主机上注册监听/usa节点的数据变化
    addWatch /usa
    
    1. 在Server1主机上修改/usa的数据
    set /usa "telangpu"
    
    1. Server3会立刻响应

      #监听到
      WatchedEvent state:SyncConnected type:NodeDataChanged path:/usa
      
    2. 如果在Server1的/usa下面创建子节点NewYork

    create /usa/NewYork
    
    1. Server3会立刻响应
    #监听到
    WatchedEvent state:SyncConnected type:NodeCreatedpath:/usa/NewYork
    
  • 删除节点

delete /usa/NewYork
  • 递归删除节点 (非空节点,节点下有子节点)
#不仅删除/ru,而且/ru下的所有子节点也随之删除
deleteall /ru

3.2 API应用

  • 主要依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.0</version>
</dependency>
3.2.1 ZooKeeper客户端
package test;


public class TestZK {
    //zookeeper集群ip与端口
    private String connectString = "IP1:2181,IP2.37:2181,IP3:2181";
    /*
    session超时 60秒:
    一定不能太少,因为连接zookeeper和加载集群环境会因为性能原因延迟略高
    如果时间太少,还没有创建好客户端,就开始操作节点,会报错的
    */
    private int sessionTimeout = 100 * 1000;
    private ZooKeeper zooKeeper;


    // 创建zookeeper客户端
    @Test
    public void init() throws IOException {
        zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            //创建监听器
            @Override
            public void process(WatchedEvent watchedEvent) {
                System.out.println("使用内部类方式创建了监听器,得到监听反馈,进行业务处理。");
                System.out.println(watchedEvent.getType());
            }
        });
    }


}

3.2.2 创建节点
  • 类似linux的文件权限,不同的是共有5种操作:CREATE、READ、WRITE、DELETE、 ADMIN(对应更改ACL的权限)。
    • OPEN_ACL_UNSAFE:创建开放节点,允许任意操作 (用的最多,其余的权限用的很少)
    • READ_ACL_UNSAFE:创建只读节点
    • CREATOR_ALL_ACL:创建者才有全部权限
@Before
public void init() throws IOException{
	// 省略...
}
   
//创建节点
    @Test
    public void createNode() throws KeeperException, InterruptedException {
        String nodeStr =
                zooKeeper.create("/lagou", "husky".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        // 参数1:要创建的节点的路径
        // 参数2:节点数据。注意:要转成字节格式
        // 参数3:节点权限 一般使用:OPEN_ACL_UNSAFE。开放的
        // 参数4:节点的类型。短暂or永久
        System.out.println(nodeStr);// /lagou
    }
3.2.3 查询节点值
   //查询节点的值
    @Test
    public void find() throws KeeperException, InterruptedException {
        byte[] bytes = zooKeeper.getData("/lagou", false, new Stat());
        String data = new String(bytes);//字节转字符
        System.out.println(data);//husky
    }

3.2.4 修改节点的值
    //更新节点的值
    @Test
    public void update() throws KeeperException, InterruptedException {
        Stat stat = zooKeeper.setData("/lagou", "husky1".getBytes(), 0);
        //0:代表当前节点的版本 使用 ls -s /lagou 查看dataVersion版本
        System.out.println(stat);//此时dataVersion版本为1
    }
3.2.5 删除节点
    //删除节点
    @Test
    public void delete() throws KeeperException, InterruptedException {
        zooKeeper.delete("/lagou",1);//先查看节点详情,获得dataVersion = 1
    }
3.2.6 获取子节点
    //获取子节点
    @Test
    public void getChildren() throws KeeperException, InterruptedException {
        List<String> list = zooKeeper.getChildren("/japan", false);
        for (String s : list) {
            System.out.println(s);  //daban Tokyo
        }
    }
3.2.7 监听子节点的变化
    //监听节点变化
    @Test
    public void watchNode () throws Exception {
        List<String> list = zooKeeper.getChildren("/", true);//开启监听
        for (String s : list) {
            System.out.println(s);
        }
        //让线程不停止
        System.in.read();//等待输入

        //NodeChildrenChanged
    }
3.2.8 判断Znode是否存在
    //判断节点是否存在
    @Test
    public void exits() throws KeeperException, InterruptedException {
        Stat stat = zooKeeper.exists("/test", false);
        System.out.println(stat == null ? "不存在" : "存在"); //存在
    }

4. 案例-模拟美团商家上下线

4.1 需求

  • 模拟美团服务平台,商家营业通知,商家打烊通知。
  • 提前在根节点下,创建好 /meituan 节点。

4.2 商家服务类

package meituan;

import org.apache.zookeeper.*;

public class ShopServer {
    private String connectString = "IP1:2181,IP2.37:2181,IP3:2181";
    private int sessionTimeout = 100 * 1000;
    private ZooKeeper zooKeeper;

    public static void main(String[] args) throws Exception{
        ShopServer shopServer=new ShopServer();

        //1.连接到集群(商家联系美团)
        shopServer.getConnection();

        //2.注册到集群(创建短暂、有序的节点类型) 入住美团
       shopServer.register(args[0]);

       //3.业务处理
        shopServer.business(args[0]);

    }
    
    //创建到zk的客户端连接
    public void getConnection() throws Exception{
        zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
            }
        });
    }
    
   //注册到集群
   public void register(String shopName)  throws Exception{
        //"EPHEMERAL_SEQUENTIAL短暂有序型"的节点。才能编号 shop1 shop2
       //并在断开时自动删除(打样)
       String createdNode =
               zooKeeper.create("/meituan/shop", shopName.getBytes(),
                       ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
       System.out.println(shopName+"开始营业了!!!"+createdNode);
       //KFE开始营业了!!!/meituan/shop0000000002   返回节点
   }

   //业务处理
    public void business(String shopName)  throws Exception{
        System.out.println(shopName+"营业中………………");
        System.in.read();
    }

}

4.3 客户类

package meituan;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.util.ArrayList;
import java.util.List;

public class Customers {
  private String connectString = "IP1:2181,IP2.37:2181,IP3:2181";
    private int sessionTimeout = 100 * 1000;
    private ZooKeeper zooKeeper;

    public static void main(String[] args) throws Exception{
        //1.获取zookeeper连接(打开美团)
        Customers client=new Customers();
        client.getConnection();

        //2.获取/meituan的所有子节点列表(获取商家列表)
        client.getShopList();

        //3.业务进程启动
        client.business();

    }

    //1.创建到zk的客户端连接
    public void getConnection() throws Exception{
        zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                //一旦节点发生变化,重新加载服务器列表信息
                try {
                    getShopList();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
    //2.获取服务器列表信息
    public void getShopList()  throws Exception{
        //1.获取服务器子节点信息,并加上监听(监听子节点的变化) KFC KFD
        List<String> shops = zooKeeper.getChildren("/meituan", true);

        //2.定义服务器列表信息(节点的数据)集合。
        ArrayList<String> shopList = new ArrayList<>();

        //3.遍历所有子节点:获取节点中的信息
        for (String shop : shops) {
            byte[] data = zooKeeper.getData("/meituan/" + shop, false, new Stat());
            shopList.add(new String(data));
        }
        //2.打印所有服务器列表信息
        System.out.println("目前正在营业的商家有:"+shopList);
       // 目前正在营业的商家有:[kfd, kfc, A]
    }

    //3.业务功能
    public void business()throws Exception{
        System.out.println("客户正在浏览商家 ...");
        System.in.read();

    }

}

4.4 解析

  • 运行客户类,就会得到商家列表

  • 首先在linux中添加(删除)一个商家,商家列表会立刻更新出最新商家。

  • 运行商家服务类,带参数启动。

5. 案例-分布式锁-商品秒杀

  • 在zookeeper中使用传统的锁引发的 “羊群效应” :1000个人创建节点,只有一个人能成功,999 人需要等待!

  • 避免“羊群效应”,zookeeper采用分布式锁。

  1. 所有请求进来,在/lock下创建临时顺序节点
  2. 判断自己是不是/lock下最小的节点
    • 是,获得锁(创建节点)
    • 否,对前面小我一级的节点进行监听
  3. 获得锁请求,处理完业务逻辑,释放锁(删除节点),后一个节点得到通知
  4. 重复步骤2

5.1 工程一览

5.2.2 Mapper接口

  • 商品表
@Mapper
@Component
public interface ProductMapper {
    // 查询商品(目的查库存)
    @Select("select * from product where id = #{id}")
    Product getProduct(@Param("id") int id);
    
    // 减库存
    @Update("update product set stock = stock-1 where id = #{id}")
    int reduceStock(@Param("id") int id);

}
  • 订单表
@Mapper
@Component
public interface OrderMapper {
    // 生成订单
    @Insert("insert into `order` (id,pid,userid) values (#{id},#{pid},#{userid})")
    int insert(Order order);
}

5.2.3 Service

@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    ProductMapper productMapper;
    @Autowired
    OrderMapper orderMapper;


    @Override
    public void reduceStock(int id) throws Exception {
      //获取库存
        Product product = productMapper.getProduct(id);

        //判断库存数量
        if(product.getStock() <= 0)
            throw new RuntimeException("已抢光!");

        // 2.减库存
        int i = productMapper.reduceStock(id);
        if(i == 1){
            Order order = new Order();
            order.setId(UUID.randomUUID().toString());
            order.setPid(id);
            order.setUserid(101);
            orderMapper.insert(order);
        }else
            throw new RuntimeException("减库存失败,请重试!");



    }
}

5.2.4高并发请求

  • 使用 JMeter 模拟1秒内发出10个http请求。

5.2.5 Controller

  • 如果不采用锁,在短时间存在大量请求时,后台数据库会出现-1字段。

  • 加锁。
@Controller
public class ProductAction {
    @Autowired
    private ProductServiceImpl productService;
    private String connectString = "IPs";

    @ResponseBody
    @GetMapping("/product/reduce")
    public Object reduce(int id) throws Exception {
        // 重试策略 (1000毫秒试1次,最多试3次)
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

        //1.创建curator工具对象
        CuratorFramework client =
                CuratorFrameworkFactory.newClient(connectString, retryPolicy);
        client.start();

        //2.根据工具对象创建“内部互斥锁”
        InterProcessMutex lock = new InterProcessMutex(client, "/product_" + id);
        try {
            //3.加锁
            lock.acquire();
            productService.reduceStock(id);
        }catch(Exception e){
            if(e instanceof RuntimeException){
                throw e;
            }
        }finally{
        //4.释放锁
            lock.release();
        }
        return "ok";

    }
}

  • 数据正常

试1次,最多试3次)
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

    //1.创建curator工具对象
    CuratorFramework client =
            CuratorFrameworkFactory.newClient(connectString, retryPolicy);
    client.start();

    //2.根据工具对象创建“内部互斥锁”
    InterProcessMutex lock = new InterProcessMutex(client, "/product_" + id);
    try {
        //3.加锁
        lock.acquire();
        productService.reduceStock(id);
    }catch(Exception e){
        if(e instanceof RuntimeException){
            throw e;
        }
    }finally{
    //4.释放锁
        lock.release();
    }
    return "ok";

}

}


+ 数据正常

[![](https://img-blog.csdnimg.cn/img_convert/fc576adc89dd638aa33ee33573b96f8a.png)](https://img.imgdb.cn/item/606ae0808322e6675c941aee.jpg)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值