第5章zookeeper分布式锁案例

什么叫做分布式锁呢?
比如说"进程 1"在使用该资源的时候,会先去获得锁,"进程 1"获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,"进程 1"用完该资源以后就将锁释放掉,让其 他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。

5.1原生Zookeeper实现分布式锁案例

在 ZooKeeper 中,获取某个节点的子节点列表和在该节点下创建子节点是可以并发同时发生的,但是需要考虑以下几点:

  1. ZooKeeper操作的串行化: ZooKeeper保证了对于同一个节点的写操作是串行化的,即同一时刻只能有一个客户端对某个节点进行写操作(创建、删除等)。这意味着如果有一个客户端正在对某个节点进行写操作(比如创建子节点),其他客户端的写操作会被阻塞,直到第一个操作完成。

  2. 读操作与写操作并发: 节点的读操作(如获取子节点列表)与写操作是可以并发进行的。即使有其他客户端在对节点进行写操作,不影响其他客户端对该节点的读操作。因此,在一个节点上同时进行读和写操作是可以的,但要注意写操作可能会影响到读取的结果,特别是当节点的数据在写操作执行过程中发生变化时。

  3. 并发操作的注意事项: 虽然获取子节点和创建子节点可以并发进行,但是在实际应用中,需要考虑并发操作可能带来的数据一致性和竞争条件。如果多个客户端同时创建相同名称的子节点,可能会导致其中一些操作失败或者产生竞争冲突。因此,应用程序需要具备一定的并发控制策略,如使用分布式锁或者版本控制来确保操作的正确性和一致性。

  4. Watch机制的影响: 在获取子节点列表或创建子节点时,可以注册Watcher来监听节点的变化。当节点的子节点发生变化时,注册的Watcher会被触发,客户端可以得到通知并进行相应处理。Watcher机制可以帮助客户端实时获取节点状态的变化,但在使用过程中也需要注意它的异步性和触发机制。

总的来说,ZooKeeper支持并发地进行读取和写入操作,但在实际开发中需要考虑并发带来的潜在竞争和一致性问题,采取适当的并发控制策略以确保数据的正确性和系统的稳定性。

(1)分布式锁实现

package com.cookie.lock2;

import com.cookie.zkcase1.DistributeServer;
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 String connectString = "192.168.1.100:2181,192.168.1.101:2181,192.168.1.102:2181";
    private int sessionTimeout = 2000;
    private ZooKeeper zkClient;
    private CountDownLatch connectLatch = new CountDownLatch(1);
    private CountDownLatch waitLatch = new CountDownLatch(1);
    private String preNodePath;
    private String thisNode;

    //获取连接,并且创建根节点
    public DistributedLock() throws IOException, InterruptedException, KeeperException {
        zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            public void process(WatchedEvent watchedEvent) {
                //说明连接上了
                if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
                    connectLatch.countDown();
                }
                //如果监听到删除节点事件,并且删除的节点路径等于当前节点的前一个节点的路径,则释放当前节点的await
                if(watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(preNodePath)){
                    System.out.println("cutThread:"+Thread.currentThread().getName()+"醒了");
                    waitLatch.countDown();
                }
            }
        });

        //此处会一直阻塞,直到connectLatch计数器的数量减少为0才会往后执行
        connectLatch.await();
        Stat stat = zkClient.exists("/locks", false);
        if(stat == null){
            System.out.println("根节点不存在");
            zkClient.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }

    }

    //加锁方法
    public void zkLock() throws InterruptedException, KeeperException {
        thisNode = zkClient.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        List<String> children = zkClient.getChildren("/locks", false);
        if(children.size() == 1){
            //获取到锁
            return;
        } else {
            Collections.sort(children);
            //获取seq-000000
            String subNode = thisNode.substring("/locks/".length());
            int index = children.indexOf(subNode);
            if(index == -1){
                System.out.println("数据异常");
            }else if(index == 0){
                //获取倒锁
                return;
            } else {
                //监听他的前一个节点
                preNodePath = "/locks/"+ children.get(index-1);
                //这里设置的true,所以监听代码在上面的process方法中
                System.out.println("curThread: "+Thread.currentThread().getName()+",curPath: "+thisNode+",preNodePath : "+preNodePath);
                zkClient.getData(preNodePath, true, null);
                //此处如果监听没有结束,需要阻塞这里
                waitLatch.await();
            }
        }
    }

    //解锁方法(删除当前节点)
    public void unZklock() throws InterruptedException, KeeperException {
        zkClient.delete(thisNode,-1);
    }
}

(2)分布式锁测试

1 )创建是个线程分别执行
package com.cookie.lock2;

import org.apache.zookeeper.KeeperException;

import java.io.IOException;

public class zkLockTest {
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {

        for(int i=0;i<10;i++){
            new Thread(new Runnable() {
                public void run() {
                    try {
                        DistributedLock lock1 = new DistributedLock();
                        lock1.zkLock();
                        System.out.println("线程"+Thread.currentThread().getName()+" 启动,获取到锁" + System.currentTimeMillis());
                        Thread.sleep(2000);
                        lock1.unZklock();
                        //System.out.println("线程"+Thread.currentThread().getName()+"释放锁"+ System.currentTimeMillis());
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } catch (KeeperException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }).start();
        }

    }
}
(2)观察控制台变化:
先通过命令./bin/zkCli.sh -server 192.168.1.100:2181登录zk客户端,查看当前集群下的节点信息如下:
然后启动我们的zkLockTest测试代码,查看控制台打印:
注意:
这里除了一个并发问题就是,如果当zk集群下没有/locks节点,我启动zkLockTest会报错提示:根节点不存在,看报错原因如下:
这是因为如果并发多,很多线程都会阻塞在下面这行

因为zk在创建、删除节点是不支持并发的,所以当其中一个线程创建好了/locks节点后,其余线程重复创建就会报这个错,所以这里我的解决方案是在启动测试代码之前,手动在zk客户端命令行添加/locks根节点,这样就可以解决。

手动创建号/locks根节点后,然后启动测试代码,控制台打印如下:

5.2Curator框架实现分布式锁案例

1)原生的 Java API 开发存在的问题

1 )会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
(2) Watch 需要重复注册,不然就不能生效
(3)开发的复杂性还是比较高的
(4)不支持多节点删除和创建。需要自己去递归

2Curator 是一个专门解决分布式锁的框架,解决了原生 JavaAPI 开发分布式遇到的问题。

详情请查看官方文档: https://curator.apache.org/index.html

3Curator 案例实操

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.cookie.lock3;

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;
import org.checkerframework.checker.units.qual.C;

public class CuratorLockTest {
    private static final String rootNode = "/locks";
    private String connectString = "192.168.1.100:2181,192.168.1.101:2181,192.168.1.102:2181";
    // connection 超时时间
    private int connectionTimeout = 2000;
    // session 超时时间
    private int sessionTimeout = 2000;

    public static void main(String[] args) {
        for(int i =0;i<10;i++){
            new Thread(new Runnable() {
                public void run() {
                    CuratorLockTest curatorLockTest = new CuratorLockTest();
                    try {
                        curatorLockTest.test();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }).start();
        }

    }

    public void test() throws Exception {
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), rootNode);
        lock1.acquire();
        System.out.println("线程"+Thread.currentThread().getName()+"获取到了锁");
        Thread.sleep(1000);
        lock1.release();
        System.out.println("线程"+Thread.currentThread().getName()+"释放了锁");
    }



    private CuratorFramework getCuratorFramework() {
        ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);

        CuratorFramework client = CuratorFrameworkFactory.builder().connectString(connectString)
                .connectionTimeoutMs(connectionTimeout)
                .sessionTimeoutMs(sessionTimeout)
                .retryPolicy(policy)
                .build();
        client.start();
        return client;
    }

}

在Apache Curator中,CuratorFramework是用于与Apache ZooKeeper交互的高级客户端。在创建CuratorFramework实例时,可以通过CuratorFrameworkFactory.builder()方法来构建客户端,并设置连接参数。

在你提供的代码片段中,.connectionTimeoutMs(connectionTimeout).sessionTimeoutMs(sessionTimeout)分别设置了连接超时和会话超时的时间。

  1. connectionTimeoutMs(connectionTimeout):这个方法设置了连接超时时间,即客户端与ZooKeeper服务器建立连接的最大等待时间。如果在这个时间内无法建立连接,将抛出连接超时异常。

  2. sessionTimeoutMs(sessionTimeout):这个方法设置了会话超时时间,即客户端与ZooKeeper服务器之间的会话保持时间。如果客户端在会话超时时间内没有发送心跳或者维持连接,ZooKeeper服务器将认为客户端已经断开,然后触发会话过期事件。

一般来说,连接超时时间应该设置得足够长以确保在网络状况不佳或者服务器负载高时仍能成功连接,会话超时时间则应根据应用程序的需求来设置,以确保会话不会因为长时间没有心跳而被关闭。

(2)观察控制台变化:
启动CuratorLockTest测试代码前,先保证zk集群已经有/locks节点,如下图:
然后启动CuratorLockTest,观察控制台打印如下:

 6.1选举机制

6.2生产集群安装多少zk合适?

6.3常用命令

ls、get、create、delete

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值