zk锁

一、分布式锁介绍

        分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性。

二、架构介绍

        在介绍使用Zookeeper实现分布式锁之前,首先看当前的系统架构图


解释: 左边的整个区域表示一个Zookeeper集群,locker是Zookeeper的一个持久节点,node_1、node_2、node_3是locker这个持久节点下面的临时顺序节点。client_1、client_2、client_n表示多个客户端,Service表示需要互斥访问的共享资源。

三、分布式锁获取思路

1.获取分布式锁的总体思路

        在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。客户端调用createNode方法在locker下创建临时顺序节点,

然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。客户端获取到所有的子节点path之后,如果发现自己在之

前创建的子节点序号最小,那么就认为该客户端获取到了锁。如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,

此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。之后,让这个被关注的节点删除,则客户端的Watcher会

收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如皋是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个

节点并注册监听。当前这个过程中还需要许多的逻辑判断。

 

2.获取分布式锁的核心算法流程

        下面同个一个流程图来分析获取分布式锁的完整算法,如下:

     解释:客户端A要获取分布式锁的时候首先到locker下创建一个临时顺序节点(node_n),然后立即获取locker下的所有(一级)子节点。

此时因为会有多个客户端同一时间争取锁,因此locker下的子节点数量就会大于1。对于顺序节点,特点是节点名称后面自动有一个数字编号,

先创建的节点数字编号小于后创建的,因此可以将子节点按照节点名称后缀的数字顺序从小到大排序,这样排在第一位的就是最先创建的顺序节点,

此时它就代表了最先争取到锁的客户端!此时判断最小的这个节点是否为客户端A之前创建出来的node_n,如果是则表示客户端A获取到了锁,

如果不是则表示锁已经被其它客户端获取,因此客户端A要等待它释放锁,也就是等待获取到锁的那个客户端B把自己创建的那个节点删除。

此时就通过监听比node_n次小的那个顺序节点的删除事件来知道客户端B是否已经释放了锁,如果是,此时客户端A再次获取locker下的所有子节点,

再次与自己创建的node_n节点对比,直到自己创建的node_n是locker的所有子节点中顺序号最小的,此时表示客户端A获取到了锁!

四、分布式锁使用

1.可重入锁Shared Reentrant Lock
2.不可重入锁Shared Lock
3.可重入读写锁Shared Reentrant Read Write Lock
4.信号量Shared Semaphore
5.多锁对象 Multi Shared Lock

1.可重入锁相关类介绍

     它是由类InterProcessMutex来实现。 它的主要方法:

// 构造方法
public InterProcessMutex(CuratorFramework client, String path)
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
// 通过acquire获得锁,并提供超时机制:
public void acquire() throws Exception
public boolean acquire(long time, TimeUnit unit) throws Exception
// 撤销锁
public void makeRevocable(RevocationListener<InterProcessMutex> listener)
public void makeRevocable(final RevocationListener<InterProcessMutex> listener, Executor executor)

错误处理:还是强烈推荐你使用ConnectionStateListener处理连接状态的改变。当连接LOST时你不再拥有锁。

2.编写示例程序
    首先让我们创建一个模拟的共享资源, 这个资源期望只能单线程的访问,否则会有并发问题。

public class FakeLimitedResource
{
    private final AtomicBoolean inUse = new AtomicBoolean(false);
    // 模拟只能单线程操作的资源
    public void use() throws InterruptedException
    {
        if (!inUse.compareAndSet(false, true))
        {
            // 在正确使用锁的情况下,此异常不可能抛出
            throw new IllegalStateException("Needs to be used by one client at a time");
        }
        try
        {
            Thread.sleep((long) (3 * Math.random()));
        }
        finally
        {
            inUse.set(false);
        }
    }
}

然后创建一个ExampleClientThatLocks类,它负责请求锁,使用资源,释放锁这样一个完整的访问过程。

public class ExampleClientThatLocks
{
    private final InterProcessMutex lock;
    private final FakeLimitedResource resource;
    private final String clientName;
    public ExampleClientThatLocks(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName)
    {
        this.resource = resource;
        this.clientName = clientName;
        lock = new InterProcessMutex(client, lockPath);
    }
    public void doWork(long time, TimeUnit unit) throws Exception
    {
        if (!lock.acquire(time, unit))
        {
            throw new IllegalStateException(clientName + " 不能得到互斥锁");
        }
        try
        {
            System.out.println(clientName + " 已获取到互斥锁");
            resource.use(); // 使用资源
            Thread.sleep(1000 * 1);
        }
        finally
        {
            System.out.println(clientName + " 释放互斥锁");
            lock.release(); // 总是在finally中释放
        }
    }
}

最后创建主程序来测试:

public class InterProcessMutexExample
{
    private static final int QTY = 5;
    private static final int REPETITIONS = QTY * 10;
    private static final String PATH = "/examples/locks";
    public static void main(String[] args) throws Exception
    {
        final FakeLimitedResource resource = new FakeLimitedResource();
        final List<CuratorFramework> clientList = new ArrayList<CuratorFramework>();
        for (int i = 0; i < QTY; i++)
        {
            CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3));
            client.start();
            clientList.add(client);
        }
        System.out.println("连接初始化完成!");
        ExecutorService service = Executors.newFixedThreadPool(QTY);
        for (int i = 0; i < QTY; ++i)
        {
            final int index = i;
            Callable<Void> task = new Callable<Void>()
            {
                @Override
                public Void call() throws Exception
                {
                    try
                    {
                        final ExampleClientThatLocks example = new ExampleClientThatLocks(clientList.get(index), PATH, resource, "Client " + index);
                        for (int j = 0; j < REPETITIONS; ++j)
                        {
                            example.doWork(10, TimeUnit.SECONDS);
                        }
                    }
                    catch (Throwable e)
                    {
                        e.printStackTrace();
                    }
                    finally
                    {
                        CloseableUtils.closeQuietly(clientList.get(index));
                    }
                    return null;
                }
            };
            service.submit(task);
        }
        service.shutdown();
        service.awaitTermination(10, TimeUnit.MINUTES);
        System.out.println("OK!");
    }
}
代码也很简单,生成5个client,每个client重复执行10次 请求锁--访问资源--释放锁的过程。每个client都在独立的线程中。
结果可以看到,锁是随机的被每个实例排他性的使用。
既然是可重入锁,你可以在一个线程中多次调用acquire,在线程拥有锁时它总是返回true。
注意:你不应该在多个线程中用同一个InterProcessMutex, 你可以在每个线程中都生成一个InterProcessMutex实例,它们的path都一样,这样它们可以共享同一个锁。
3.示例运行结果
    运行结果控制台如下:

连接初始化完成!
Client 4 已获取到互斥锁
Client 4 释放互斥锁
Client 3 已获取到互斥锁
Client 3 释放互斥锁
......
Client 2 已获取到互斥锁
Client 2 释放互斥锁
OK!

运行时查看Zookeeper节点信息如下:


2.不可重入锁Shared Lock

这个锁和上面的相比,就是少了Reentrant的功能,也就意味着它不能在同一个线程中重入。这个类是InterProcessSemaphoreMutex使用方法和上面的类类似

    首先我们将上面的例子修改一下,测试一下它的重入。修改ExampleClientThatLocks.doWork,连续两次acquire:

public void doWork(long time, TimeUnit unit) throws Exception
{
    if (!lock.acquire(time, unit))
    {
        throw new IllegalStateException(clientName + " 不能得到互斥锁");
    }
    System.out.println(clientName + " 已获取到互斥锁");
    if (!lock.acquire(time, unit))
    {
        throw new IllegalStateException(clientName + " 不能得到互斥锁");
    }
    System.out.println(clientName + " 再次获取到互斥锁");
    try
    {
        resource.use(); // 使用资源
        Thread.sleep(1000 * 1);
    }
    finally
    {
        System.out.println(clientName + " 释放互斥锁");
        lock.release(); // 总是在finally中释放
        lock.release(); // 获取锁几次 释放锁也要几次
    }
}
注意:我们也需要调用release两次。这和JDK的ReentrantLock用法一致。如果少调用一次release,则此线程依然拥有锁。
上面的代码没有问题,我们可以多次调用acquire,后续的acquire也不会阻塞。
但是将上面的InterProcessMutex换成不可重入锁InterProcessSemaphoreMutex,如果再运行上面的代码,结果就会发现线程被阻塞在第二个acquire上,直到超时。也就是此锁不是可重入的。

3.可重入读写锁Shared Reentrant Read Write Lock

类似JDK的ReentrantReadWriteLock。 一个读写锁管理一对相关的锁。一个负责读操作,另外一个负责写操作。读操作在写锁没被使用时可同时由多个进程使用,而写锁在使用时不允许读(阻塞)。
    此锁是可重入的。一个拥有写锁的线程可重入读锁,但是读锁却不能进入写锁。 这也意味着写锁可以降级成读锁, 比如请求写锁 --->读锁 ---->释放写锁。从读锁升级成写锁是不行的。
1.可重入读写锁相关类介绍
    可重入读写锁 主要由两个类实现: InterProcessReadWriteLock、InterProcessMutex 使用时首先创建一个InterProcessReadWriteLock实例,然后再根据你的需求得到读锁或者写锁,读写锁的类型是InterProcessMutex
2.编写示例程序
    示例程序仍使用上面的FakeLimitedResource、InterProcessMutexExample类


public class ExampleClientReadWriteLocks
{
    private final InterProcessReadWriteLock lock;
    private final InterProcessMutex readLock;
    private final InterProcessMutex writeLock;
    private final FakeLimitedResource resource;
    private final String clientName;
    public ExampleClientReadWriteLocks(CuratorFramework client, String lockPath, FakeLimitedResource resource, String clientName)
    {
        this.resource = resource;
        this.clientName = clientName;
        lock = new InterProcessReadWriteLock(client, lockPath);
        readLock = lock.readLock();
        writeLock = lock.writeLock();
    }
    public void doWork(long time, TimeUnit unit) throws Exception
    {
        // 注意只能先得到写锁再得到读锁,不能反过来!!!
        if (!writeLock.acquire(time, unit))
        {
            throw new IllegalStateException(clientName + " 不能得到写锁");
        }
        System.out.println(clientName + " 已得到写锁");
        if (!readLock.acquire(time, unit))
        {
            throw new IllegalStateException(clientName + " 不能得到读锁");
        }
        System.out.println(clientName + " 已得到读锁");
        try
        {
            resource.use(); // 使用资源
            Thread.sleep(1000 * 1);
        }
        finally
        {
            System.out.println(clientName + " 释放读写锁");
            readLock.release();
            writeLock.release();
        }
    }
}
在这个类中我们首先请求了一个写锁,然后降级成读锁。执行业务处理,然后释放读写锁。修改 InterProcessMutexExample类中的ExampleClientThatLocks为ExampleClientReadWriteLocks然后运行示例。
3. 示例运行结果
    运行结果控制台:

连接初始化完成!
Client 1 已得到写锁
Client 1 已得到读锁
Client 1 释放读写锁
......
Client 3 已得到写锁
Client 3 已得到读锁
Client 3 释放读写锁
OK!

此时查看Zookeeper数据节点如下:



4.信号量Shared Semaphore

一个计数的信号量类似JDK的Semaphore。JDK中Semaphore维护的一组许可(permits),而Cubator中称之为租约(Lease)。
    有两种方式可以决定semaphore的最大租约数。第一种方式是有用户给定的path决定。第二种方式使用SharedCountReader类。
    如果不使用SharedCountReader,没有内部代码检查进程是否假定有10个租约而进程B假定有20个租约。 所以所有的实例必须使用相同的numberOfLeases值.
1.信号量实现类说明
主要类有:
  • InterProcessSemaphoreV2 - 信号量实现类
  • Lease - 租约(单个信号)
  • SharedCountReader - 计数器,用于计算最大租约数量
    这次调用acquire会返回一个租约对象。客户端必须在finally中close这些租约对象,否则这些租约会丢失掉。但是,如果客户端session由于某种原因比如crash丢掉,那么这些客户端持有的租约会自动close,这样其它客户端可以继续使用这些租约。
租约还可以通过下面的方式返还:

public void returnLease(Lease lease)
public void returnAll(Collection<Lease> leases)

注意一次你可以请求多个租约,如果Semaphore当前的租约不够,则请求线程会被阻塞。同时还提供了超时的重载方法。

public Lease acquire() throws Exception
public Collection<Lease> acquire(int qty) throws Exception
public Lease acquire(long time, TimeUnit unit) throws Exception
public Collection<Lease> acquire(int qty, long time, TimeUnit unit) throws Exception

2.编写示例程序

public class InterProcessSemaphoreExample
{
    private static final int MAX_LEASE = 10;
    private static final String PATH = "/examples/locks";
    public static void main(String[] args) throws Exception
    {
        FakeLimitedResource resource = new FakeLimitedResource();
        CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", new ExponentialBackoffRetry(1000, 3));
        client.start();
        InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, PATH, MAX_LEASE);
        Collection<Lease> leases = semaphore.acquire(5);
        System.out.println("获取租约数量:" + leases.size());
        Lease lease = semaphore.acquire();
        System.out.println("获取单个租约");
        resource.use();
        Collection<Lease> leases2 = semaphore.acquire(5, 10, TimeUnit.SECONDS);
        System.out.println("获取租约,如果为空则超时: " + leases2);
        System.out.println("释放租约");
        semaphore.returnLease(lease);
        System.out.println("释放集合中的所有租约");
        semaphore.returnAll(leases);
        client.close();
        System.out.println("OK!");
    }
}
首先我们先获得了5个租约, 接着请求了一个租约,因为semaphore还有5个租约,所以请求可以满足,返回一个租约,还剩4个租约。
然后再请求一个租约,因为租约不够,阻塞到超时,还是没能满足,返回结果为null。
3.示例运行结果
    运行结果控制台如下:

获取租约数量:5
获取单个租约
获取租约,如果为空则超时: null
释放租约
释放集合中的所有租约
OK!

  此时查看Zookeeper数据节点如下:


注意:上面所讲的4种锁都是公平锁(fair)。从ZooKeeper的角度看,每个客户端都按照请求的顺序获得锁。相当公平。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值