分布式锁详解 - 分别利用Zookeeper和数据库实现分布式锁

一、锁的相关描述
1、锁的使用流程:

首先参考一下JDK所实现的锁的通用接口Lock.java接口:

/*
 * @since 1.5
 * @author Doug Lea
 */
public interface Lock {

    /**
     * 加锁
     */
    void lock();


    /**
     *  锁中断
     */
    void lockInterruptibly() throws InterruptedException;


    /**
     * 获取锁
     */
    boolean tryLock();


    /**
     *  获取锁(可以传入等待时间)
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;


    /**
     *  释放锁
     */
    void unlock();

}

线程对锁的操作流程大体如下(是个递归的流程):

① 竞争锁:任务通过竞争获取锁才能对该资源进行操作;
② 占有锁:当有一个任务在对资源进行更新时;
③ 任务阻塞:其他资源进入阻塞状态,都不会对这个资源进行操作;
④ 释放锁:更新任务完成后,对锁进行释放。

(PS:平时可以借助countDownLatch(发令枪)进行高并发场景的模拟。
在这里插入图片描述

2、问题:

JVM内置的锁(Lock实现类(如ReentrantLock)、synchronized关键字等),都存在一个共同的问题。就是当应用分片部署到多机器的情况下,JVM内置锁就起不了作用。这时候就需要用到分布式锁了。

二、分布式锁实现方案
1、常见的分布式锁有如下几种:

在这里插入图片描述

在实现上面几种分布式锁之前,会涉及到模板方法这个设计模式。
如果对模板方法不熟悉的,可以看下我的这篇博文,很简单。
模板方法(Template Pattern)的使用:https://blog.csdn.net/qq_33404395/article/details/83310947

2、分布式锁的实现

可以先定义一个抽象分布式锁,然后不同方案的分布式锁各自去实现其抽象方法。(模板方法

在这里插入图片描述

实现如下:


import java.util.concurrent.locks.Lock;

/**
 * @author WangCw
 * @create 2019-04-22 19:52
 * @description
 **/
public abstract class AbstractLock implements Lock {

    public void getLock(){
        //尝试获取锁资源
        if(tryLock()){
            System.out.println("获取Lock锁的资源 #####");
        } else {
            //等待
            waitLock();
            //重新获取锁资源(等待后递归获取锁)
            getLock();
        }
    }

    //获取锁资源
    public abstract boolean tryLock();
    
    //等待
    public abstract void waitLock();

     //释放锁
    public abstract void unLock();


}

3、借助mysql实现简单的分布式锁

1、新建一张Mysql的表 t_lock,仅需要一个主键字段,id

2、继承上述锁的抽象类,然后实现tryLock()、waitLock()和unLock()方法,分布式锁的实现就基本完成。

3、代码实现如下(伪代码)


class MysqlLock extends AbstractLock{
    
    //获取锁的方法  ---> insert 一条 id=1的记录,成功返回 true,否则 false;
    public  boolean tryLock(){
        try{
           insert(id=1);
           return true;
        }catch (Exception e){
           //主键是唯一索引 重复插入会报错
           return false;
        }
    }


    // 等待获取锁的方法,一段缓存时间,防止系统频繁去获取锁
    public  void waitLock() throw Exception{
        Thread.sleep(3000);
    }

    //释放锁 --->  delete一条 id=1的纪录
    public  void unLock() throw Exception{
        delete(id=1);
    }

}


如上,这就借助mysql完成一个很简单的分布式锁了。但是问题来了,上面的锁缺点很明显。
性能差,700并发基本就封顶了,还有锁表中的数据没被删除的时候数据库就挂了或者服务器挂了,妥妥来一个死锁。实现上也非常的不优雅

基本市面上很少公司会使用数据库做分布式锁,现在分布式锁主要还是借助zookeeper、Redis来实现。

三、借助zookeeeper实现分布式锁
1、zookeeper节点类型:

在这里插入图片描述
简而言之,就是持久、持久带序号、临时、临时带序号四种类型的节点。
常用操作如下:

创建持久节点 (存放到磁盘)
create /test value

创建有序的持久节点
create -s /test value

创建临时节点(服务端与客户端断开链接就丢失),临时节点如果已经存在就会创建失败
create -e /test value

创建有序的临时节点,临时节点如果已经存在,创建不会失败,会在节点后面添加另一个序号
create -s -e /test value
2、zookeeper的工作模式 - watcher(观察者模式)

服务注册通知
在这里插入图片描述

3、zookeeper实现分布式锁

同样的,利用zookeeper实现分布式锁,继承上述的抽象类AbstractLock,然后实现其对应抽象方法(模版方法)
伪代码如下:


class ZookeeperLock extends AbstractLock{

    private final String PATH = "/lock";
    private CountDownLatch countDownLatch = null;
    
    //获取锁的方法  ---> 插入一条临时节点,成功返回 true,否则 false;
    public  boolean tryLock(){
        try{
           create -e /lock lock ;
           return true;
        }catch (Exception e){
           //已经存在该临时节点会出现异常
           return false;
        }
    }


    // 等待获取锁的方法。利用zk的监听机制,等待可以再次插入临时节点时,再进行tryLock()获取锁
    public  void waitLock() throw Exception{
        
        //创建监听器去监听临时节点 /lock ,当节点被删除,就唤醒其他线程
        IZKDataListener izkDataListener = new IZKDataListener(){

            public void handleDataDeleted(String path) throws Exception{
                //唤醒被等地啊的线程
                if(countDownLatch != null){
                    countDownLatch.countDown();
                }
            }

        };

        //注册事件
        zkClient.subscribeDAtaChanges(PATH, izkDataListener);

        //如果节点存在
        if(zkClient.exists(PATH)){
            countDownLatch = new CountDownLatch(1);
            try{
                //一直等待事件通知
                countDownLatch.await();
            } catch(Exception e){
                e.printStackTrace();
            } 
        }
        //删除监听
        zkClient.unsubscribeDataChanges(PATH, izkDataListener);

    }


    //释放锁 --->  删除tryLock()中插入的临时节点
    public  void unLock() throw Exception{
        1、delete /lock;
        2、客户端与服务端断开;//释放资源
    }

}

上面就是借助ZK实现的分布式锁,性能远大于数据库实现的,并且也相对更加稳定。但是还是存在问题,上面unLock()方法调用后,会Watch机制会通知到所有的线程,导致一个问题 – “羊群效应”。


四、羊群效应及解决方案
1、zookeeper的节点类型中,临时顺序节点有如下特点:

(1)节点的生命周期和客户端会话绑定,即创建节点的客户端会话一旦失效,那么这个节点也会被清除;
(2)每个父节点都会负责维护其子节点创建的先后顺序,并且如果创建的是顺序节点(SEQUENTIAL)的话,父节点会自动为这个节点分配一个整形数值,以后缀的形式自动追加到节点名中,作为这个节点最终的节点名。

2、一般借助Zookeeper实现分布式锁的基本逻辑如下:

(1)、客户端调用create()方法创建名为“_locknode_/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL
(2)、客户端调用getChildren(“_locknode_”)方法来获取所有已经创建的子节点,同时在这个节点上注册上子节点变更通知的Watcher
(3)、客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,那么就认为这个客户端获得了锁。
(4)、如果在步骤3中发现自己并非是所有子节点中最小的,说明自己还没有获取到锁,就开始等待,直到下次子节点变更通知的时候,再进行子节点的获取,判断是否获取锁。
(5)、释放锁的过程相对比较简单,就是删除自己创建的那个子节点即可。

3、羊群效应

上面这个分布式锁的实现中,大体能够满足了一般的分布式集群竞争锁的需求。这里说的一般性场景是指集群规模不大,一般在10台机器以内。

不过,细想上面的实现逻辑,我们很容易会发现一个问题,步骤4,“即获取所有的子点,判断自己创建的节点是否已经是序号最小的节点”,这个过程,在整个分布式锁的竞争过程中,大量重复运行,并且绝大多数的运行结果都是判断出自己并非是序号最小的节点,从而继续等待下一次通知——这个显然看起来不怎么科学。客户端无端的接受到过多的和自己不相关的事件通知,这如果在集群规模大的时候,会对Server造成很大的性能影响,并且如果一旦同一时间有多个节点的客户端断开连接,这个时候,服务器就会像其余客户端发送大量的事件通知——这就是所谓的羊群效应

4、分布式锁实现逻辑改进

下面是改进后的分布式锁实现,和之前的实现方式唯一不同之处在于,这里设计成每个锁竞争者,只需要关注”_locknode_”节点下序号比自己小的那个节点是否存在即可。实现逻辑如下:
(1)、客户端调用create()方法创建名为“_locknode_/guid-lock-”的节点,需要注意的是,这里节点的创建类型需要设置为EPHEMERAL_SEQUENTIAL
(2)、客户端调用getChildren(“_locknode_”)方法来获取所有已经创建的子节点,注意,这里不注册任何Watcher
(3)、客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点序号最小,那么就认为这个客户端获得了锁。
(4)、如果在步骤3中发现自己并非所有子节点中最小的,说明自己还没有获取到锁。此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时注册事件监听。
(5)、之后当这个被关注的节点被移除了,客户端会收到相应的通知。这个时候客户端需要再次调用getChildren(“_locknode_”)方法来获取所有已经创建的子节点,确保自己确实是最小的节点了,然后进入步骤3。


五、zk分布式锁依旧存在的问题

虽然上面面的流程看起来天衣无缝,但是zk还是存在一个安全隐患。就是加锁期间,业务还未执行完,应用和zk服务因网络波动断开连接,导致锁被自动释放,然而业务仍在继续执行。并且其他线程也能获取锁,导致有并发问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值