分布式锁实现方案

在实际生产中,尤其是分布式环境下,因为我们逻辑真正处理的业务数据是只有一份的,接口并发时势必会出现并发问题,使得业务数据不正确,这个时候就需要一种类似于锁的东西来保证数据的幂等性,比如秒杀业务。实现分布式锁的方式非常多,zookeeper、redis、数据库等均可,如果使用zookeeper原生方式来实现的话还是比较复杂的,基于这种场景,我们利用Apache的开源客户端Curator来实现分布式锁。首先来回顾一下zookeeper的相关知识:

zookeeper的四种节点类型

1、持久化节点 :所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。

2、持久化顺序节点:这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。基于持久顺序节点原理的经典应用-分布式唯一ID生成器

3、临时节点:和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点,集群zk环境下,同一个路径的临时节点只能成功创建一个,利用这个特性可以用来实现master-slave选举

4、临时顺序节点:相对于临时节点而言,临时顺序节点比临时节点多了个有序,也就是说每创建一个节点都会加上节点对应的序号,先创建成功,序号越小。其经典应用场景为实现分布式锁。(zk实现分布式锁的大致原理可引申阅读:https://www.cnblogs.com/ysw-go/p/11444993.html

监视器(watcher)

当zookeeper创建一个节点时,会注册一个该节点的监视器,当节点状态发生改变时,watch会被触发,zooKeeper将会向客户端发送一条通知(就一条,因为watch只能被触发一次)。

原理

Curator内部是通过InterProcessMutex(可重入锁)来在zookeeper中创建临时有序节点实现的,之前说过,如果通过临时节点及watch机制实现锁的话,这种方式存在一个比较大的问题:所有取锁失败的进程都在等待、监听创建的节点释放,很容易发生"羊群效应",zookeeper的压力是比较大的,而临时有序节点就很好的避免了这个问题,Curator内部就是创建的临时有序节点。

基本原理:

创建临时有序节点,每个线程均能创建节点成功,但是其序号不同,只有序号最小的可以拥有锁,其它线程只需要监听比自己序号小的节点状态即可

基本思路如下:

1、在你指定的节点下创建一个锁目录lock;

2、线程X进来获取锁在lock目录下,并创建临时有序节点;

3、线程X获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁;

4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应);

5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。

代码实现

<dependency>
   <groupId>org.apache.curator</groupId>
   <artifactId>curator-recipes</artifactId>
   <version>2.4.1</version>
</dependency>
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
 * classname:DistributedLock
 * desc:基于zookeeper的开源客户端Cruator实现分布式锁
 * author:simonsfan
 */
public class DistributedLock {
    public static Logger log = LoggerFactory.getLogger(DistributedLock.class);
    private InterProcessMutex interProcessMutex;  //可重入排它锁
    private String lockName;  //竞争资源标志
    private String root = "/distributed/lock/";//根节点
    private static CuratorFramework curatorFramework;
    private static String ZK_URL = "zookeeper1.tq.master.cn:2181,zookeeper3.tq.master.cn:2181,zookeeper2.tq.master.cn:2181,zookeeper4.tq.master.cn:2181,zookeeper5.tq.master.cn:2181";
    static{
        curatorFramework= CuratorFrameworkFactory.newClient(ZK_URL,new ExponentialBackoffRetry(1000,3));
        curatorFramework.start();
    }

    /**
     * 实例化
     * @param lockName
     */
    public DistributedLock(String lockName){
        try {
            this.lockName = lockName;
            interProcessMutex = new InterProcessMutex(curatorFramework, root + lockName);
        }catch (Exception e){
            log.error("initial InterProcessMutex exception="+e);
        }
    }

    /**
     * 获取锁
     */
    public void acquireLock(){
        int flag = 0;
        try {
            //重试2次,每次最大等待2s,也就是最大等待4s
            while (!interProcessMutex.acquire(2, TimeUnit.SECONDS)){
                flag++;
                if(flag>1){  //重试两次
                    break;
                }
            }
        } catch (Exception e) {
           log.error("distributed lock acquire exception="+e);
        }
         if(flag>1){
              log.info("Thread:"+Thread.currentThread().getId()+" acquire distributed lock  busy");
         }else{
             log.info("Thread:"+Thread.currentThread().getId()+" acquire distributed lock  success");
         }
    }

    /**
     * 释放锁
     */
    public void releaseLock(){
        try {
        if(interProcessMutex != null && interProcessMutex.isAcquiredInThisProcess()){
            interProcessMutex.release();
            curatorFramework.delete().inBackground().forPath(root+lockName);
            log.info("Thread:"+Thread.currentThread().getId()+" release distributed lock  success");
        }
        }catch (Exception e){
            log.info("Thread:"+Thread.currentThread().getId()+" release distributed lock  exception="+e);
        }
    }
}

业务层使用时要记得释放锁。要特别注意的是 interProcessMutex.acquire(2, TimeUnit.SECONDS)方法,可以设定等待时候,加上重试的次数,即排队等待时间= acquire × 次数,这两个值一定要设置好,因为使用了分布式锁之后,接口的TPS就下降了,没获取到锁的接口都在等待/重试,如果这里设置的最大等待时间4s,这时并发进来1000个请求,4秒内处理不完1000个请求怎么办呢?所以一定要设置好这个重试次数及单次等待时间,根据自己的压测接口设置合理的阈值,避免业务流转发生问题!


参考资料:Apache Curator http://curator.apache.org/

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐