【zookeeper】Apache curator的使用及zk分布式锁实现

接上篇,本篇主要讲Apache开源的curator的使用,有了curator,利用Java对zookeeper的操作变得极度便捷.

其实在学之前我也有个疑虑,我为啥要学curator,撇开涨薪这些外在的东西,就单技术层面来讲,学curator能帮我做些什么?这就不得不从zookeeper说起,上篇我已经大篇幅讲了zk是做什么的了,但真正要靠zk去实现多服务器自动拉取更新的配置文件等功能是非常难的,如果没有curator,直接去写的话基本上能把你累哭,就好比连Mybatis或者jpa都没有,让你用原生的代码去写个网站一样,你可以把curator当做一个比较强大的工具,有了它操作zk不再是事,说这么多,是时候进入正题了:

curator 官网:http://curator.apache.org

使用curator去实现的几块内容:


学习目录:
1.使用curator建立与zk的连接
2.使用curator添加/递归添加节点
3.使用curator删除/递归删除节点
4.使用curator创建/验证 ACL(访问权限列表)
5.使用curator监听 单个/父 节点的变化(watch事件)
---------------------------------------------
6.基于curator实现zookeeper分布式锁(需要掌握基本的多线程知识)

前置条件:已掌握zookeeper的基本操作,对zookeeper有所了解,如果没有掌握请翻阅我前面的博客去学习.

本节所需要引入的依赖有以下三个,建议直接全部引入即可:

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.12</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.0.0</version>
        </dependency>

1.通过curator建立与zk的连接

需要准备连接zk的url,建议直接写成工具类,因为接下来会频繁用到,功能类似于jdbc.

public class ZkClientUtil {
    private static final int BASE_SLEEP_TIME_MS = 5000; //定义失败重试间隔时间 单位:毫秒
    private static final int MAX_RETRIES = 3; //定义失败重试次数
    private static final int SESSION_TIME_OUT = 1000000; //定义会话存活时间,根据业务灵活指定 单位:毫秒
    private static final String ZK_URI = "192.168.174.132:2181";//你自己的zkurl和端口号
    private static final String NAMESPACE = "laohan_jianshen";
    //工作空间,可以不指定,建议指定,功能类似于项目包,之后创建的所有的节点都会在该工作空间下,方便管理
    
    public static CuratorFramework build(){
    //创建比较简单,链式编程,很爽,基本上指定点参数就OK了
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(BASE_SLEEP_TIME_MS,MAX_RETRIES);//重试策略
        CuratorFramework client = CuratorFrameworkFactory
                .builder()
                .connectString(ZK_URI)
                .retryPolicy(retryPolicy)
                .namespace(NAMESPACE)
                .sessionTimeoutMs(SESSION_TIME_OUT)
                .build();
        return client;
    }
}

2.通过curator添加/递归添加节点

//通过上一步获取到的client,直接启动该client,值得注意的是client在使用前必须先启动:
client.start;
client
.create()//创建节点
.withMode(CreateMode.xxx)//节点属性:永久节点/临时节点/有序节点 通过CreateMode.即可看到
.withACL(ZooDefs.Ids.xxx)//节点访问权限,通过Ids.即可看到 默认是OPEN_ACL_UNSAFE(开放不安全权限)
.forPath("/yourpath","yourdata".getBytes());//指明你的节点路径,数据可以不指定,数据必须是byte[]

创建递归节点:

//比如我想一次性创建/yourpath/a/b/c/1/2/3...这样的节点,如果按传统方法会累死你
//curator可以一次性创建好,只需要在创建时添加creatingParentsIfNeeded即可.
client
.create()//创建节点
.creatingParentsIfNeeded()//创建父节点,如果需要的话

...

3.使用curator删除/递归删除节点

client
.delete() //删除
.guaranteed()//保证一定帮你删了它
.withVersion(0)//指定要删节点的版本号
.forPath("/yourpath")//指定要删节点的路径

递归删除:

//比如我当前的节点结构是这样:/yourpath/a/b/c/1/2/3  我想删除a节点下面的所有目录
//传统方法累死个人,现在只需要添加deletingChildrenIfNeeded即可
client
.delete() //删除
.deletingChildrenIfNeeded()//如果它有儿子都给删了...

4.使用curator创建/验证 ACL(访问权限列表)

//为了保证安全,有时需要对节点的访问权限做一些限制,否则可能会引起重要信息泄露/篡改/删除等
//节点ACL的创建方式有两种,一种是使用ZK提供的,一种是自定义的
//1.ZK提供的,比较简单,拿来即用,在创建节点时指明withACL即可
client
.create()
.withACL(ZooDefs.Ids.READ_ACL_UNSAFE)//指明该节点是只读节点,还有其他属性,可以通过Ids.查看
//创建自定义ACL,需要自己new Id(),并指明是否是加密的,然后账号和密码是多少,加密策略使用zk提供的:
List<ACL> aclList = new ArrayList<ACL>();
ACL acl1 = new ACL(ZooDefs.Perms.READ,new Id("digest",DigestAuthenticationProvider.generateDigest("user:123456")));
ACL acl2 = new ACL(ZooDefs.Perms.ALL,new Id("digest",DigestAuthenticationProvider.generateDigest("root:123456")));
aclList.add(acl1);
aclList.add(acl2);
//如此我就创建好了两种不同的权限账号,user只能对该节点有读的权限,但root用户对该节点有所有权限
//ACL验证,创建好节点之后,可以在服务器的zk安装目录的bin目录下 连接客户端./zkCli
//然后通过ls /该目录  查看是否可以访问 正常是不能访问的 会提示权限不够
//下面我们通过curator去连接,要想访问该节点需要在创建client时就指明账号和密码:
CuratorFramework client = CuratorFrameworkFactory
.builder()
.authorization("digest","root:123456".getBytes())//指明使用了加密,用户名和密码用:隔开,以byte[]输入
//如此,接下来通过该client可以对刚刚创建的节点具有所有权限,如果登录的是user,则只具有读权限.

5.通过curator创建单个节点及其父节点的watch事件

由于zk的watch事件是只能被触发一次的,触发完即销毁监听,这显然不是我们想要的,在实际开发中更多的场景是需要对某个节点持续监听,所以这里我只介绍创建持续监听的单节点/父节点

//对单个节点创建watch事件
//定义NodeCache,指明被监听节点的路径:
final NodeCache nodeCache = new NodeCache(client,"/yourpath");
nodeCache.start(true);//开启
nodeCache
.getCurrentData()//可以获取该监听节点的数据
.getPath();//可以获取该监听节点的路径

//对指定父节点创建watch事件,只要其任何一个子节点,或子节点的子节点...发生变化,就会触发watch事件.
//定义PathChildrenCache,指明要watch的目录
final PathChildrenCache pathChildrenCache = new PathChildrenCache(client,"yourpath");
//启动,启动策略有三种:同步,异步提交,异步 用的比较多的就是下面这种,用StartMode.可以查看到
pathChildrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
//对该节点创建监听器
pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
    public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                    //TODO 可以通过PathChildrenCacheEvent.拿到你想要的数据和路径等
    }
});

至此,curator的常用内容已学习完毕,建议每个都亲自操作一下,为之后的自动配置和分布式锁操作打下基础.

 


6.基于curator实现zookeeper分布式锁

先来了解下分布式锁应用场景:

比如我有一个电商商城,为了提高系统的服务量,业务被拆分小了,分别部署在不同的服务器上,下单成功后订单系统通知库存系统要及时减库存,但此时并发极高,库存系统还没来得及减库存,又有新的人进来了,读取了库存,数据被脏读了,于是他也下单成功了,但发货的时候发现库存不够了,于是闹得两家都不开心...这时候就需要分布式锁来解决.

为了演示这样的场景,我写了个小项目来模拟这种场景:

我数据库里电脑库存只有10台,然后写了两个下单页面,每个订单买8台电脑,让线程适当休眠来模拟高并发下的延迟,于是我几乎同时访问了这两个下单页面,最后两边都提示我下单成功了,但数据库里的库存数量变成了-6

 

这显然不是我想要的结果,正确的应该是,这两个人里只有一个人下单成功,另外一个人下单失败,提示库存不足,最后数据库里剩2台电脑库存.

为了解决这个问题,就必须让第一个人下单时,第二个人不能下单,只能等到第一个人下单完成后方可下单,或者第二个人下单时,第一个人不能下单,只能等到第二人下单完成方可下单,由于两套系统是分开部署的,不能像以前那样用同步锁/同步代码块Synchronized来解决了,这个时候就需要引出分布式锁,分布式锁可以用Redis或者zookeeper等实现,这篇主要讲一下用zk去实现.

思路:提供一把全局的锁,所有来购买的请求竞争这一把锁,谁先拿到这把锁,谁就有资格执行下单,没抢到锁的请求被挂起,等待有锁的请求完成下单后释放锁,然后唤醒被挂起的请求继续去竞争这把锁...

可以把这把锁当做是zk上的一个节点,所有请求发起时,创建该节点,第一个创建该节点成功的请求就意味着获得了锁,其他请求创建都会抛出异常,然后捕获该异常,用全局的countDownLatch将该请求挂起,等获得锁的节点完成下单后,把该节点删除(释放锁),然后计数器-1,把挂起的线程都唤醒,继续去竞争该锁...

下面就顺着这个思路一起去实现分布式锁:

这里默认使用上面已经写好的连接ZK的工具类来创建client.

public class  ZkLockUtil {
    //分布式锁,用于挂起当前线程,等待上一把分布式锁释放
    private static CountDownLatch DISTRIBUTE_LOCK = new CountDownLatch(1);
    //分布式锁的总结点名
    private final static String ZK_LOCK_PROJECT = "zk-lock";
    //分布式锁节点名
    private final static String DISTRIBUTE_LOCK_NAME = "distribute-lock";
    /**
     * 获取分布式锁
     */
    public static void getLock() {
        CuratorFramework client = ZkClientUtil.build();
        client.start();
        while (true) {
            try { 
                   client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath("/" + ZK_LOCK_PROJECT + "/" + DISTRIBUTE_LOCK_NAME);
                System.out.println("获取分布式锁成功...");
                return;
            } catch (Exception e) {
                try {
                    //如果没有获取到锁,需要重新设置同步资源值
                    if (DISTRIBUTE_LOCK.getCount() <= 0) {
                        DISTRIBUTE_LOCK = new CountDownLatch(1);
                    }
                    System.out.println("获取分布式锁失败,等待他人释放锁中...");
                    DISTRIBUTE_LOCK.await();
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
            }
        }
    }

    /**
     * 释放锁资源
     */
    public static void  release(String path) {
        CuratorFramework client = ZkClientUtil.build();
        client.start();
        try {
            client.delete().forPath(path);
            System.out.println("锁释放成功...");
        } catch (Exception e) {
            System.out.println("释放锁失败...");
            e.printStackTrace();
        } finally {
            client.close();
        }
    }

    /**
     * 为指定路径节点创建watch,观察锁状态
     */
    public static void addWatcher2Path(final String path) throws Exception {
        CuratorFramework client = ZkClientUtil.build();
        client.start();
        final PathChildrenCache pathChildrenCache = new PathChildrenCache(client, path, true);
        pathChildrenCache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
        System.out.println("创建观察者成功...");
        pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                if (pathChildrenCacheEvent.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
                    String nodePath = pathChildrenCacheEvent.getData().getPath();
                    System.out.println("上一会话已释放锁或会话已断开...节点路径为:" + nodePath);
                    if (nodePath.contains(DISTRIBUTE_LOCK_NAME)) {
                        DISTRIBUTE_LOCK.countDown();
                        System.out.println("释放计数器,计数器值为:"+DISTRIBUTE_LOCK.getCount()+"让当前请求来获取分布式锁...");
                    }
                }
            }
        });
    }
}

下面来测试一下,有空的话你可以写一个类似我这种下单的模式去测试,如果时间紧写个测试类模拟也无妨:

public class Test {
    public static void main(String[] args) {
        final ExecutorService threadpool = Executors.newCachedThreadPool();
        System.out.println("开始购买...");
        for (int i = 0; i <2 ; i++) {
            threadpool.execute(new Runnable() {
                public void run() {
                    System.out.println("我是线程:"+Thread.currentThread().getName()+"我开始抢购了...");
                     ZkLockUtil.getLock();
                    System.out.println(Thread.currentThread().getName()+":我正在疯狂的剁手购买中...");
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":我买完了,有请下一位...");
                    try {
                        ZkLockUtil.addWatcher2Path("/zk-lock");
                        System.out.println("添加完毕...");
                        ZkLockUtil.release("/zk-lock/distribute-lock");
                        System.out.println("释放完毕...");
                        Thread.sleep(1000);

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

测试结果可以看出,在其中一个线程购买时,另外一个线程被挂起等待...

或者用我这种2个服务一起下单模式的:

@RestController
public class SellController {
    @Autowired
    private SellRepository sellRepository;
    @RequestMapping("sell")
    public String sell(@RequestParam("num")Integer num){
        String msg = "下单成功!";
        try {
            Thread.sleep(3000);
            ZkLockUtil.getLock();
            Sell sell = sellRepository.getOne(1);
            int remain = sell.getStock()-num;
            sell.setStock(remain);
            if (remain >= 0){
                sellRepository.save(sell);
            }else{
                msg = "下单失败,库存不足...";
            }
            ZkLockUtil.addWatcher2Path("/zk-lock");
            ZkLockUtil.release("/zk-lock/distribute-lock");
        } catch (Exception e) {
            e.printStackTrace();
            msg = "下单异常...";
        }
        return msg;
    }

测试结果:

数据库中电脑库存由原来的10变为2,达到期望效果:

但这样就大功告成了吗? 其实我觉得没有,原因是因为我在测试的时候发现一个问题,当我在浏览器中按住F5进行刷新页面(模拟高并发下的请求频率),在一开始一切都是正常的,刷一阵子之后八尔哥就出来了:

找了一下原因,发现问题出在了这里:

由于我不断的刷新页面,就意味着不断的去获取锁和释放锁,当锁被释放后计数器减1,会去唤醒线程去竞争锁,然后这个时候来没来的及唤醒,新的请求又进来了,此时新请求创建锁成功了,被唤醒的线程又抢不到锁了,但计数器仍处于0的状态,它会继续去创建锁,此时又有新的请求不断进来,不断创建锁...导致zk认为你是在不断的进行重复操作,于是它就把连接给退出了,然后又有新请求进来了,又要重新建立连接:

新建立的连接又会像上面一样在连绵不断的请求中断开,这样频繁的连接和断开,重复数次之后,ZK直接关闭了连接,导致后台无限报错...

为了解决这个问题,我搜罗各大网站,最后没有找到什么可以参考的东西,我甚至开始怀疑是我写的锁有问题,但后来我在apache curator官网找到了解释,其实curator已经帮我们封装好了一套分布式锁,可以直接拿来用的:

于是我直接调用了zk封装好的这一套分布式锁去做测试,普通情况下跟我自己写的分布式锁没啥两样,效果一样,然后我继续用F5模拟高并发下的情况,结果跟我自己的锁如出一辙...也是报同样的错,错误原因也一样,最后阅读了下源码,其实自己写的跟apache写的没啥两样,原理都一样,只是人家封装的更方便别人使用一些...

    @RequestMapping("plus")
    public String sellPlus(@RequestParam("num")int num) throws Exception {
        CuratorFramework client = ZkClientUtil.build();
        client.start();
        String lockPath = "/plus_lock";
        InterProcessMutex lock = new InterProcessMutex(client, lockPath);
        String msg = "下单成功!";
        if ( lock.acquire(3, TimeUnit.SECONDS) )
        {
            try
            {
                Sell sell = sellRepository.getOne(1);
                int remain = sell.getStock()-num;
                if (remain >= 0){
                    sell.setStock(remain);
                    sellRepository.save(sell);
                }else {
                    msg = "下单失败,库存不足...";
                }
            }
            finally
            {
                lock.release();
            }
        }
        return msg;
    }

我总不能去怀疑apache 写的锁也有问题吧,那问题应该就出在了zk建立连接或者zk过滤连接的机制上,应该是zk以为那些频繁关闭又连接的请求是被攻击或者无效的请求,所以强制关闭这些连接,目前尚未去研究zk的连接机制,也不清楚研究了是否能解决该问题,所以基于zk的分布式锁就讲到这里,同时在我心里它已经不是做分布式锁的首选了,我会考虑使用redis或者其它分布式锁去解决,尤其是在高并发的情况下,感兴趣的可以继续关注本博,在redis系列教程中,我会讲如何用redis实现分布式锁.

<think>好的,我现在需要帮助用户理解ZooKeeper实现分布式锁的原理和方法,并提供代码示例。用户之前提到了Redis的WAIT指令,现在转向ZooKeeper,可能是在比较不同分布式锁实现。根据提供的引用内容,用户已经了解了一些ZooKeeper的基本概念和优缺点,比如临时节点、Watch机制、羊群效应等。 首先,我需要整理ZooKeeper实现分布式锁的核心原理。根据引用内容,ZooKeeper的临时节点和顺序节点是关键。临时节点在客户端断开时自动删除,避免锁无法释放的问题。顺序节点可以用来实现公平锁,避免羊群效应。Watch机制用于监听节点变化,减少轮询开销。 接下来是具体实现步骤。用户可能需要了解如何创建临时顺序节点,判断是否获得锁,以及如何处理等待。这里需要结合引用的内容,比如使用Apache Zookeeper包或者Curator框架。引用3提到了原生API和Curator两种方式,用户可能对两者都有兴趣,但Curator更简便,所以应该重点介绍。 关于代码示例,用户可能希望看到原生API和Curator的对比。原生API的示例需要展示创建节点、设置Watcher、使用CountDownLatch等,但代码可能较为复杂。而Curator封装了这些逻辑,代码更简洁,引用3和4也提到了Curator,所以应该推荐使用Curator。 注意事项部分需要涵盖羊群效应、性能影响和异常处理。引用4提到原生实现可能导致羊群效应,而Curator通过监听前一个节点来避免,这一点很重要。性能方面,ZooKeeper的强一致性可能影响速度,需要权衡。异常处理包括连接丢失和节点删除,确保锁的正确释放。 最后,相关问题需要引导用户深入思考,比如对比Redis和ZooKeeper的锁,如何优化性能,处理脑裂问题等。这些问题帮助用户扩展知识,解决可能遇到的实际问题。 总结下来,回答需要结构清晰,分原理、实现步骤、代码示例、注意事项和相关问题。确保引用内容被正确参考,比如临时节点、Watch机制、Curator使用,以及羊群效应的解决方案。同时,保持语言简洁,代码示例明确,注意事项突出关键点,问题部分引导深入学习。</think>### ZooKeeper 分布式锁实现原理与方法详解 --- #### **一、核心原理** ZooKeeper 通过**临时顺序节点**和**Watcher 监听机制**实现分布式锁,保障**强一致性**和**可靠性**[^1][^2]。 1. **临时顺序节点**(Ephemeral Sequential Node) - 锁资源对应一个持久节点(如 `/locks`),客户端在竞争锁时创建临时顺序子节点(如 `/locks/lock_00000001`)。 - **临时性**:客户端断开连接时节点自动删除,避免锁泄漏[^2]。 - **顺序性**:节点按创建顺序编号,天然支持公平锁。 2. **Watcher 监听机制** - 客户端仅监听**前一个顺序节点**(如当前节点为 `lock_00000002`,则监听 `lock_00000001`)。 - 前序节点释放锁(节点被删除)时,触发 Watcher 通知后续节点竞争锁,避免“羊群效应”[^4]。 --- #### **二、实现步骤** 以下为基于 ZooKeeper 原生 API 的锁实现流程: 1. **初始化锁路径** ```java String lockPath = "/locks/resource_name"; String currentNode = zk.create(lockPath + "/lock_", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); ``` 2. **判断是否获得锁** ```java List<String> children = zk.getChildren(lockPath, false); Collections.sort(children); // 按序号排序 if (currentNode.equals(lockPath + "/" + children.get(0))) { // 当前节点是序号最小的节点,获得锁 } else { // 监听前一个节点 String previousNode = children.get(Collections.binarySearch(children, currentNode) - 1); zk.exists(lockPath + "/" + previousNode, new LockWatcher()); // 阻塞等待(如通过 CountDownLatch) } ``` 3. **释放锁** ```java zk.delete(currentNode, -1); // 删除临时节点 ``` --- #### **三、代码示例(Curator 框架)** Apache Curator 封装了 ZooKeeper 锁逻辑,推荐生产环境使用[^3]: ```java public class DistributedLockExample { public static void main(String[] args) { CuratorFramework client = CuratorFrameworkFactory.newClient( "localhost:2181", new RetryNTimes(3, 1000) ); client.start(); InterProcessMutex lock = new InterProcessMutex(client, "/locks/resource_name"); try { if (lock.acquire(10, TimeUnit.SECONDS)) { // 尝试获取锁,最多等待10秒 // 执行业务逻辑 System.out.println("Lock acquired, processing..."); } } finally { lock.release(); // 释放锁 client.close(); } } } ``` --- #### **四、注意事项** | 场景 | 说明 | |---------------------|----------------------------------------------------------------------| | **羊群效应** | 原生实现需避免监听所有节点,Curator 已通过顺序监听优化[^4] | | **性能影响** | ZooKeeper 强一致性导致写入延迟较高,适用于低频高可靠场景 | | **异常处理** | 需处理网络断开、Session 超时等场景,确保锁能正确释放 | --- #### **五、相关问题** 1. ZooKeeper 分布式锁与 Redis 分布式锁的核心区别是什么?[^1][^2] 2. 如何解决 ZooKeeper 集群脑裂(Split-Brain)导致的锁失效问题? 3. Curator 框架中 `InterProcessReadWriteLock` 如何实现读写锁分离? 4. ZooKeeper 的临时节点机制如何避免锁的永久死锁?[^2] --- **引用文献** [^1]: Zookeeper 分布式锁类型与实现原理 [^2]: 临时节点与 Watcher 机制设计 [^3]: Curator 框架封装逻辑与 API 使用 [^4]: 原生实现与羊群效应优化
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值