zookeeper入门学习

本文主要简单的介绍了zookeeper的相关操作,比如zookeeper基本操作,分布式锁实现,leader选举机制,有错误的地方,欢迎大家指正。


一、zookeeper 概述:

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是常见的Hadopp和Hbase框架的重要组件。ZooKeeper提供了一系列的功能,比如:数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

二、zookeeper 数据结构:

ZooKeeper的数据结构很类似一个标准的linux文件系统。它文件路劲为:/+节点名称。比如:/zookeeper
在这里插入图片描述

三、CAP理论:

一个分布式系统应该基于以下三点来设计,但是只能同时满足其中的两点:

  • 一致性(Consistency):一个分布式系统应该在多个副本上保持数据一致。也就是说,无论客户端访问哪个副本,得到的数据应该是一致的。一致性分为强一致性、弱一致性、最终一致性,而zookeeper在数据一致性上选择的是最终一致性。
  • 可用性(Availability):保证客户端的每一次请求都得到响应,并且拿到数据,但是不保证每次拿到的数据都是最新的。
  • 分区容错性(Partition tolerance):分布式系统中部分系统出现网络出现故障时,也要能保障剩余的服务对外提供正常的一致性服务,除非整个网络故障,或者整个集群崩溃。

对于分布式系统来说,P是必须的,所以只能是保证CP或者AP,zookeeper保证的是CP。

四、session原理

客户端和服务端基于TCP协议进行长连接,客户端默认连接服务端的2181端口,也就是session会话。从第一次连接开始,session生命周期就开始,服务端开始维护这个session会话。

1、session的创建:

  • sessionID:session的每一次创建都会产生一个sessionID,用来唯一标示这个session会话。

  • Timeout:客户端在连接服务端的时候可以向服务端传递一个会话超时时间,服务端会根据会话限制确定最终的会话超时时间。

  • TickTime:下次会话超时时间点,默认 2000 毫秒。可在 zoo.cfg 配置文件中配置,便于 server 端对 session 会话实行分桶策略管理

  • isClosing:该属性标记一个会话是否已经被关闭,当 server 端检测到会话已经超时失效,该会话标记为"已关闭",不再处理该会话的新请求。

2、session的状态:

  • connecting:连接中,session 一旦建立,状态就是 connecting 状态,时间很短。
  • connected:已连接,连接成功之后的状态。
  • closed:已关闭,发生在 session 过期,一般由于网络故障客户端重连失败,服务器宕机或者客户端主动断开。

五、zookeeper 分桶策略管理和会话激活

1、分桶策略:

zookeeper中将session过期时间在某一时间段的session放在同一个集合中,实际上是一个ConcurrentHashMap,key为时间段的最大的session过期时间,当currentTime达到这个最大的session过期时间时,就执行一次会话过期处理任务,然后将这个最大的session过期时间下的所有的会话设置为过期。

2、会话激活

客户端会在过期时间范围内,对服务端发送请求或者ping请求,从而保持会话的有效性。这个过程又称心跳检测。

六、zookeeper 基本命令

  • create [-s|-e] /test/test “test” acl:创建test,并设置节点 test 的值为 test 。-s 代表顺序节点, -e 代表临时节点,注意其中 -s 和 -e 可以同时使用的,并且临时节点不能再创建子节点。acl 访问权限相关,默认是 world,相当于全世界都能访问
    在这里插入图片描述
  • ls /test:查看目录 test ,查看的是目录下的子目录的信息。
    在这里插入图片描述
  • set /test/test “210221” [version]:设置test目录下的值。version 为数据版本,可选。
    在这里插入图片描述
  • get [-s|-w] /test:获取 test 目录下设置的值。并监听这个目录进行监听。 [-s|-w]参数可选。

在这里插入图片描述

  • delete /test [version]:删除节点 test ,version 为数据版本,可选。

在这里插入图片描述

七、节点特性

节点特性

1、同级节点key唯一:

2、 创建节点时,必须要带上全路径:

3、 session 关闭,临时节点清除:

4、 自动创建顺序节点:

5、 watch 机制,监听节点变化:

6、 delete 命令只能一层一层删除,deleteall 可以做到递归删除:

特别说明节点的字段附带信息含义:

cZxid创建节点时的事务ID
ctime创建节点时的时间
mZxid最后修改节点时的事务ID
mtime最后修改节点时的时间
pZxid表示该节点的子节点列表最后一次修改的事务ID,添加子节点或删除子节点就会影响子节点列表,但是修改子节点的数据内容则不影响该ID**(注意,只有子节点列表变更了才会变更pzxid,子节点内容变更不会影响pzxid)**
cversion子节点版本号,子节点每次修改版本号加1
dataversion数据版本号,数据每次修改该版本号加1
aclversion权限版本号,权限每次修改该版本号加1
ephemeralOwner创建该临时节点的会话的sessionID。**(**如果该节点是持久节点,那么这个属性值为0)
dataLength该节点的数据长度
numChildren该节点拥有子节点的数量**(只统计直接子节点的数量)**

Zab协议下 节点有三种状态

  • Following:当前节点是跟随者,服从 Leader 节点的命令。
  • Leading:当前节点是 Leader,负责协调事务。
  • Election/Looking:节点处于选举状态,正在寻找 Leader。
  • Observing : Zookeeper 引入 Observer ,Observer 不参与选举,是只读节点,跟 Zab 协议没有关系。

基于以上特性,如下场景可以应运zookeeper

  • 数据发布/订阅
  • 负载均衡
  • 分布式协调/通知
  • 集群管理
  • master 管理
  • 分布式锁
  • 分布式队列

八、权限控制 ACL

ACL是zookeeper的权限控制表,ACL可以针对节点设置访问权限,保障数据的安全性。

1、ACL构成:

acl 通过**[scheme🆔permissions]** 来构成权限列表。

  • scheme:代表采用的某种权限机制,包括 world、auth、digest、ip、super 几种。
  • id:代表允许访问的用户。
  • permissions:权限组合字符串,由 cdrwa 组成,其中每个字母代表支持不同权限, 创建权限 create©、删除权限 delete(d)、读权限 read®、写权限 write(w)、管理权限admin(a)。

2、ACL命令:

  • getAcl 命令:获取某个节点的 acl 权限信息。

在这里插入图片描述

  • setAcl 命令:设置某个节点的 acl 权限信息。

在这里插入图片描述

  • addauth 命令:输入认证授权信息,注册时输入明文密码,加密形式保存。

在这里插入图片描述

九、watcher事件原理

两种方式添加watch:一种在new Zookeeper的时候,默认的watcher,这种watcher是全局性的,另外一种是使用getData等api添加watch。zookeeper下的watch实现是采用观察者模式,客户端维护着一个ZKWatchManager,服务端维护着一个WathcManager。

客户端的watcher会被存放在客户端的ZKWatchManager集合中,服务端接收到请求的时候判断需不需要注册watcher,如果需要注册watcher,又会将watcher事件存放在服务端的WathcManager集合中,当服务端处理完请求的时候,就通知客户端,客户端拿到服务端的请求的时候,通过路径查找这个watcher,然后执行相应的方法。

客户端注册watcher:

以getData为例,客户端通过getData注册的watcher,该watcher暂时被封装到WatchRegistration中,通过path和watcher建立对应关系,并且该请求会被标识成使用watcher监听的请求。后续这个WatchRegistration继续被封装成packet,packet会被添加到队列中,最终客户端和服务端就是通过packet进行传输。

ZooKeeper:

public byte[] getData(String path, Watcher watcher, Stat stat) throws KeeperException, InterruptedException {
        PathUtils.validatePath(path);
        ZooKeeper.WatchRegistration wcb = null;
        if (watcher != null) {
            wcb = new ZooKeeper.DataWatchRegistration(watcher, path); // 将watcher事件包装成WatchRegistration
        }
	…………省略其他代码…………
    }

ClientCnxn:

public ClientCnxn.Packet queuePacket(RequestHeader h, ReplyHeader r, Record request, Record response, AsyncCallback cb, String clientPath, String serverPath, Object ctx, WatchRegistration watchRegistration, WatchDeregistration watchDeregistration) {
    …………省略其他代码…………
     this.outgoingQueue.add(packet); // 客户端最终将请求封装成packet,添加到队列中
    …………省略其他代码…………
    }

    this.sendThread.getClientCnxnSocket().packetAdded();
    return packet;
}

客户端通过getData注册的watcher,那么每次都需要重新创建watcher,那么这些watcher并不是直接发送到服务端的,packet的封装并没有将客户端的wacher封装到packet中,只是将header和request封装到了packet中。

ClientCnxn.Packet类:

public void createBB() {
    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
        boa.writeInt(-1, "len");
        if (this.requestHeader != null) {
            this.requestHeader.serialize(boa, "header"); // 封装请求头
        }

        if (this.request instanceof ConnectRequest) {
            this.request.serialize(boa, "connect");
            boa.writeBool(this.readOnly, "readOnly");
        } else if (this.request != null) {
            this.request.serialize(boa, "request"); // 封装请求
        }

        baos.close();
        this.bb = ByteBuffer.wrap(baos.toByteArray());
        this.bb.putInt(this.bb.capacity() - 4);
        this.bb.rewind();
    } catch (IOException var3) {
        ClientCnxn.LOG.warn("Ignoring unexpected exception", var3);
    }

}

服务端处理watcher:

在STEW的逻辑处理处理中,首先会判断是不是需要注册watcher监听。如果需要,服务端就会将path和对应请求的serverxcnx添加到WathcManager中。

服务端将读取到的请求封装并存储到对列中,等待异步线程处理,对于读请求,将本机数据返回给客户端,对于写请求,就将事务写入本机,并最终调用cnxn.sendResponse将相应结果发送到客户端。

ServerCnxn:

public void sendResponse(ReplyHeader h, Record r, String tag) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    BinaryOutputArchive bos = BinaryOutputArchive.getArchive(baos);

    try {
        baos.write(fourBytes);
        bos.writeRecord(h, "header");
        if (r != null) {
            bos.writeRecord(r, tag);
        }

        baos.close();
    } catch (IOException var8) {
        LOG.error("Error serializing response");
    }

    byte[] b = baos.toByteArray();
    this.serverStats().updateClientResponseSize(b.length - 4);
    ByteBuffer bb = ByteBuffer.wrap(b);
    bb.putInt(b.length - 4).rewind();
    this.sendBuffer(bb);
}

客户端读取响应:

客户端通过

客户端最终会调用SendThread线程读取服务端响应结果。拿到响应结果之后,将watcher从结果中获取出来注册到客户端的ZKWatchManager中,并且还会确定事件类型,并将watcher从datawatcher中删除,这就是说明,getData注册的wathcher是一次性的。

SendThread:

void readResponse(ByteBuffer incomingBuffer) throws IOException { // SendThread.readResponse读取服务端的响应
  		…………省略其他代码…………
        } finally {
            ClientCnxn.this.finishPacket(packet); // 从所有的watcher事件中分离本次的watcher事件
        }
    }
}

客户端将watcher分离出来之后,EventThread又会将这些watcher添加到waitingEvents这个队列中,EventThread是一个线程,会不断从waitingEvents对列中获取watcher执行其中的process方法,这个时候,就能执行到我们客户端注册watcher时自定义的process方法了。

ClientCnxn:

public void queuePacket(ClientCnxn.Packet packet) {
    if (this.wasKilled) {
        synchronized(this.waitingEvents) {
            if (this.isRunning) {
                this.waitingEvents.add(packet);
            } else {
                this.processEvent(packet);
            }
        }
    } else {
        this.waitingEvents.add(packet); // 客户端继续封装,将服务端的packet添加到waitingEvents这个队列
    }
}

EventThread:

private void processEvent(Object event) {
    try {
        if (event instanceof ClientCnxn.WatcherSetEventPair) {
            ClientCnxn.WatcherSetEventPair pair = (ClientCnxn.WatcherSetEventPair)event;
            Iterator var3 = pair.watchers.iterator();  

            while(var3.hasNext()) { // 遍历所有的watcher
                Watcher watcher = (Watcher)var3.next();

                try {
                    if (watcher != null && pair.event != null) {
                        watcher.process(pair.event); // 循环执行每个watcher的process方法,这里实际上执行的就是自定义的process方法。
                    }
                } catch (Throwable var11) {
                    ClientCnxn.LOG.error("Error while calling watcher ", var11);
                }
            }
}

十、数据同步原理

1、ZAB协议

Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播)。ZAB协议可以实现的前提ZK集群可以正常对外提供服务。Zookeeper就是根据Za协议来保证数据最终一致性。Zab协议提供两种模式:崩溃恢复和消息广播。当zk集群启动时或者leader服务器出现崩溃时,根据Zab协议,zk集群会进入崩溃恢复模式,选举出新的leader,当选举出新的leader,并且有超过集群一半数量的服务器完成和该leader数据同步的后,zk集群退出崩溃恢复模式,进入消息广播模式,整个zk集群正常对外提供服务。如果此时,加入一台服务器,则这台服务器直接进入崩溃恢复模式,和leader进行数据同步,之后,整个集群正常提供服务。

2、数据同步过程

2.1、崩溃恢复—数据恢复

Zab协议的消息广播是原子性的,在一般情况下是不会有任何问题的,但是一旦leader出现崩溃的情况或者leader和超过集群数量一半的服务器失去了联系,那么这个leader的存在就没有意义了。此时集群进入崩溃恢复模式,重新选举leader。重新选举leader需要解决以下几个问题:

  • 已经处理的消息不能被丢失。

    如果leader接收到客户端请求生成了proposal并发送给了其他follower节点后,在发送commit命令之后,突然就崩溃了,而此时,follower1节点刚好处理完了commit请求,follower2节点还没处理完commit请求。这种情况下,这条消息是不能丢弃的,那么Zab协议就必须保证选举出zxid最大的一个节点为leader,也就是follower1节点,从而保证整个集群在恢复后能从follower1节点恢复数据。

  • 已经丢弃的消息不能再次出现。

    如果leader接收到客户端请求生成了proposal之后就崩溃了,因为其他的节点没有接受到请求,那么在重新选举leader之后,这条消息是被跳过的。而原leader节点恢复后注册成了follower节点,但是发现自己的状态和整个集群的状态不一致,那么此时它需要删除。

2.2、消息广播—原子广播

​ 消息广播实际上是一个简化版的2PC的过程:

​ (1)leader接收到客户端请求后,将请求转化成一个事务 proposal 提议并先持久化在自己的服务器上,并且分配一个64位的自增的全局唯一的id,叫做zxid。

​ (2)因为leader和每个follower之间都维护着一个FIFO的队列。leader将proposal处理完后,就会将proposal发送到各个follower节点上。

​ (3)follower节点接收到proposal后,会以事物的方式进行持久化,成功之后,会向leader发送一个响应,也就是ack。

​ (4)当leader接收到超过集群数量一半的follower的ack之后,就认为这个消息已经处理完成。此时,leader就可以发送commit命令。

​ (5)leader向所有的follower节点发送commit命令,同时自身也进行事物提交,即将此次消息写入内存,并向发起响应。

​ 这里可以看出,leader在接收到半数以上的ack之后就会发送commit 命令,之后,不再关注follower节点是不是commit事物成功,从而得出,zk保证的是数据最终一致性。

十一、Leader选举机制

选举条件:

​ 3.1、zxid最新的选择leader,事物id,自增,处理 proposal的时候生成。

一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 leader 选举产生一个新的 leader,新 leader 会将 epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新 leader 选举后这个值重置为 0。

​ 3.2、myid大的选择leader。

注意:3台机器的集群中,如果先启动的两台机器已经完成了选举工作,那么第三台启动后再不会参与竞争选举,即使它的myid是更大的或者zxid最新,而是会将zxid同步成leader的zxid,源码位置:Follower.java(followLeader())。

总结:

  • 新选举出来的 Leader 不能包含未提交的 Proposal
  • 新选举的 Leader 节点中含有最大的 zxid

十二、2PC

4.1、预提交:leader本地自己先持久化数据,并将请求发送给所有的follower,其他的follower收到命令后,也进行持久化,成功后,向leader响应一个ack。如果这一步发送的时候,发现无法连接到其他的节点,那么说明集群服务基本瘫痪,leader会自己shurdown,因为它自存在的已经没有意义了。

4.2、收到回复(源码位置:Leader.java(processAck()):leader接收follower的ack,如果成功的接收到超过半数的ack,那就说明消息处理完成,此时,leader需要再次发送commit命令给所有的follower,告诉所有的follower本次事务可以进行最终的提交了。但是,却不再关注follower是否最终提交成功。

  • 过半机制: 3台机器的集群中,如果两台成功的回复了ack,当然也包含leader自身的ack,那么leader就认为请求已经处理OK。源码位置QuorumMaj.java(containsQuorum())

4.3、提交/回滚:follower收到commit命令后,对本次事务进行最终的提交,也就是将数据写入内存。但是ZK中没有回滚机制。

  • 同步机制:所有的zk服务器,都应该以leader服务器的状态和数据为准。

十三、Zookeeper 集群 处理请求的过程

5.1、当一个服zk服务器leader接收到写请求时:

​ (1)leader接收到客户端请求后,将请求转化成一个事务 proposal 提议并先持久化在自己的服务器上,并且分配一个64位的自增的全局唯一的id,叫做zxid。

​ (2)因为leader和每个follower之间都维护着一个FIFO的队列。leader将proposal处理完后,就会将proposal发送到各个follower节点上。

​ (3)follower节点接收到proposal后,会以事物的方式进行持久化,成功之后,会向leader发送一个响应,也就是ack。

​ (4)当leader接收到超过集群数量一半的follower的ack之后,就认为这个消息已经处理完成。此时,leader就可以发送commit命令。

​ (5)leader向所有的follower节点发送commit命令,同时自身也进行事物提交,即将此次消息写入内存,并向发起响应。

5.2、当一个zk服务器中的follower接收到写请求时:

(1)follower会将请求直接发送给leader,因为follower没有写请求的权限。之后的处理流程和5.1一样。

5.3、zk服务器中的observe接收到写请求时:执行5.2的流程。

十四、Zookeeper 分布式锁实现原理

分布式锁的总结:分布式锁就是将多个进程间并行的操作转化成串行的操作。Zookeeper能实现分布式锁有一下三主要因素::

1、create -s -e /locks/lock:

2、规定序号最小的获取锁。每个节点都去监听比自己小的节点的,挡比自己小的节点消失时,判断自身是不是变成了最小的节点,如果是,那么就获取锁。

Zookeeper API实现分布式锁:

/**
 * 用来获取客户端链接
 */
public class ZKApiClient {

    /**
     * 定义临时有序节点的父节点
     */
    private static final String locks = "/api_lock";

    /**
     * 获取客户端链接
     *
     * @return 客户端链接实例
     */
    public static ZooKeeper getZk() throws IOException, InterruptedException, KeeperException {
        ZooKeeper zk = new ZooKeeper("192.168.93.227:2181", 20000, watchedEvent -> {
        });

        /**
         * 直到客户端链接成功之后才去创建父节点
         */
        while (!zk.getState().isConnected()) {
            /**
             * 如果父节点没有创建,则去创建
             */
            if (zk.getState().isConnected()) {
                Stat stat = zk.exists(locks, false);
                if (stat == null) {
                    zk.create(locks, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }
            }
        }
        return zk;
    }
}


public class ZKApiLock {

    /**
     * 存放线程实例
     */
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    /**
     * 模拟库存,库存增减必须保证原子性
     */
    private static final AtomicInteger count = new AtomicInteger(2);

    /**
     * 定义临时有序节点的父节点
     */
    private static final String locks = "/lock";

    /**
     * 主方法入口
     *
     * @param args 参数
     */
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        /**
         * 定义5个线程,模拟客户端去对库存进行竞争操作。
         */
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    process(ZKApiClient.getZk());
                } catch (IOException | InterruptedException | KeeperException e) {
                    e.printStackTrace();
                }
            }, "thread" + i).start();
            countDownLatch.countDown();
        }
    }

    /**
     * 尝试加锁
     *
     * @param zk 客户端链接
     * @return 是否加锁成功
     */
    private static boolean tryLock(ZooKeeper zk) throws KeeperException, InterruptedException {
        try {
            /**
             * 支付的时候,必须保证线程安全。
             */
            if (lock(zk)) {
                System.out.println(Thread.currentThread().getName() + "-" + threadLocal.get() + "获取到了锁");
                pay();
                return true;
            }
        } finally {
            unlock(zk);
        }
            return false;

    }

    /**
     * 加锁逻辑
     *
     * @param zk
     * @return 是否加锁成功
     */
    private static boolean lock(ZooKeeper zk) {
        try {
            /**
             * 每个线程先去创建属于自己的临时顺序节点
             */
            String selfNode = zk
                    .create(locks + "/", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            /**
             * 保存线程自己创建的临时节点
             */
            threadLocal.set(selfNode);
            /**
             * 休眠一会,保证拿到了所有的线程创建的临时顺序节点
             */
            Thread.sleep(4);
            /**
             * 查询所有的节点信息
             */
            List<String> children = zk.getChildren(locks, false);
            /**
             * 对节点信息进行排序,排序号,第一个节点就是序号最小的
             */
            Collections.sort(children);
            System.out.println(Thread.currentThread().getName() + "创建节点为" + selfNode + ":所有的节点信息为" + children);
            String s = children.get(0);
            /**
             * 如果自己创建的临时顺序节点就是最小的那个节点,便获得锁
             */
            if (threadLocal.get().equals(locks + "/" + s)) {
                return true;
            }

            NumberFormat formatter = NumberFormat.getNumberInstance();
            formatter.setMinimumIntegerDigits(10);
            formatter.setGroupingUsed(false);
            int index = threadLocal.get().indexOf("/", 2) + 1;
            String self = threadLocal.get().substring(index, 16);
            /**
             * 如果自己创建的临时顺序节点不是最小的那个节点,便获取比他自己小1的那个节点,
             * 并监听那个节点的变化。
             */
            String prvNode = formatter.format(Integer.parseInt(self) - 1);
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            /**
             * 监听比他自己小1那个节点的变化,当那个节点别删除的时候,比他自己小1那个节点
             * 就会通知他继续竞争锁。
             */
            Stat exists = zk.exists(locks + "/" + prvNode, watchedEvent -> {
                if (Watcher.Event.EventType.NodeDeleted.equals(watchedEvent.getType())) {
                    System.out.println(
                            "前一个节点 " + prvNode + " 被删除了 -" + Thread.currentThread().getName() + "被唤醒!唤醒节点信息为:" + self);
                    countDownLatch.countDown();
                }
            });

            /**
             * 如果存在比他自己小1那个节点,就等待那个节点被删除时通知他继续竞争锁。
             */
            if (exists != null) {
                System.out.println(Thread.currentThread().getName() + "-" + threadLocal.get() + "等待锁!开始监听" + prvNode + "节点变化");
                countDownLatch.await();
            }
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }

        return true;
    }

    /**
     * 解锁逻辑。删除自己创建的那个节点信息,删除自己创建的链接
     *
     * @param zk
     */
    private static void unlock(ZooKeeper zk) throws InterruptedException, KeeperException {
        zk.delete(String.valueOf(threadLocal.get()), -1);
        zk.close();
    }

    /**
     * 逻辑入口
     *
     * @param zk 客户端链接
     */
    static void process(ZooKeeper zk) {
        try {
            /**
             * 5个线程可以在不保证线程安全的情况下,去查询库存,
             * 有点类似抢票的时候,明明看到有余票,但是下单支付的时候,却提示无余票,支付失败。
             */
            System.out.println(Thread.currentThread().getName() + "查询库存:" + getInventory());
            tryLock(zk);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取当前剩余库存
     *
     * @return 剩余库存
     */
    private static int getInventory() {
        return count.get();
    }

    /**
     * 订单支付
     */
    private static void pay() {
        /**
         * 支付的时候,再次判断下是否还有余票。无余票则支付失败。
         */
        if (getInventory() == 0) {
            System.out.println(Thread.currentThread().getName() + "库存为0,支付失败.");
            return;
        }
        System.out.println(Thread.currentThread().getName() + "库存为," + count + "支付成功");
        /**
         * 支付成功后,库存减1
         */
        count.decrementAndGet();
    }
}

Zookeeper Curator框架实现分布式锁:

/**
 * 用来获取客户端链接
 */
public class ZKCuratorClient {
    private static final String CONNECT_STRING = "192.168.93.227:2181";

    /**
     * 获取客户端链接
     *
     * @return 客户端链接实例
     */
    public static CuratorFramework getZk() {
        CuratorFramework client = CuratorFrameworkFactory.builder().connectString(CONNECT_STRING)
                .sessionTimeoutMs(100 * 1000)
                .retryPolicy(new RetryNTimes(10, 2 * 1000))
                .build();

        client.start();

        return client;
    }
}

public class ZKCuratorLock {

    /**
     * 定义临时有序节点的父节点
     */
    private static final String LOCK_PATH = "/curator_lock";

    /**
     * 模拟库存,库存增减必须保证原子性
     */
    private static final AtomicInteger count = new AtomicInteger(2);

    /**
     * 主方法入口
     *
     * @param args 参数
     */
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(5);
        /**
         * 定义5个线程,模拟客户端去对库存进行竞争操作。
         */
        for (int i = 0; i < 5; i++) {
            new Thread(() -> process(ZKCuratorClient.getZk()), "thread" + i).start();
            countDownLatch.countDown();
        }
    }

    /**
     * 逻辑入口
     *
     * @param client 客户端链接
     */
    private static void process(CuratorFramework client) {
            /**
             * 5个线程可以在不保证线程安全的情况下,去查询库存,
             * 有点类似抢票的时候,明明看到有余票,但是下单支付的时候,却提示无余票,支付失败。
             */
            System.out.println(Thread.currentThread().getName() + "查询库存:" + getInventory());
            tryLock(client);
    }

    /**
     * 获取当前剩余库存
     *
     * @return 剩余库存
     */
    private static int getInventory() {
        return count.get();
    }

    /**
     * 订单支付
     */
    private static void pay() {
        /**
         * 支付的时候,再次判断下是否还有余票。无余票则支付失败。
         */
        if (getInventory() <= 0) {
            System.out.println(Thread.currentThread().getName() + "库存为0,支付失败.");
            return;
        }
        System.out.println(Thread.currentThread().getName() + "库存为," + count + "支付成功");
        /**
         * 支付成功后,库存减1
         */
        count.decrementAndGet();
    }

    private static boolean tryLock(CuratorFramework client) {
        try {
            return lock(client);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    private static boolean lock(CuratorFramework client) throws Exception {
        InterProcessLock processLock = new InterProcessMutex(client, LOCK_PATH);
        try {
            if (processLock.acquire(10 * 1000, TimeUnit.SECONDS)) {
                pay();
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            unlock(processLock);
        }
        return false;
    }

    private static void unlock(InterProcessLock processLock) throws Exception {
        processLock.release();
    }
}

总结

本文主要是针对zookeeper做了一些简单的学籍总结,适合初学者参考。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜间沐水人

文章编写不易,一分钱也是爱。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值