基于 ZooKeeper 的分布式锁简单实现

**欢迎关注公众号**
**微信扫一扫**

1.锁的概念

  • 在单进程的系统中,当存在多个线程可以同时改变某个变量(共享内存变量)时,就需要对变量或代码块做同步,使得多个线程在修改共享变量时能够线性执行,以防并发修改变量的值。
  • 而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
  • 不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如 Java 中 synchronize 是在对象头设置标记,Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每个线程都能拥有对该 int 的可见性和原子修改,操作系统例如linux 内核中也是利用互斥量或信号量等内存数据做标记
  • 除了利用内存数据做锁其实任何互斥的都能做锁(只考虑互斥情况),如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁,或者使用某个文件是否存在作为锁等。只需要满足在对标记进行修改能保证原子性和内存可见性即可。

2.分布式

  • 在集群模式下,多个相同服务同时开启.在许多的场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。很多时候我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,通过 Java 提供的并发 API 我们可以解决,但是在分布式环境下,应用进程跑在不同的jvm中,就不像单体应用那么简单了。
  • 分布式与单机情况下最大的不同在于其不是多线程而是多进程。多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。

3.分布式锁

  • 在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。

  • 与单机模式下的锁相比,不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。分布式场景下问题复杂性的根源之一主要就是需要考虑到网络的延时和不可靠性。

  • 分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。

使用zookeeper实现分布式锁


1.实现一个基本的分布式锁

  • 原理:利用zookeeper临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁。
  • 缺点:所有取锁失败的进程都监听父节点,很容易发生羊群效应,即当释放锁后所有等待进程一起来创建节点,并发量很大。

2.实现一个优化的分布式锁

  • 原理:上锁改为创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 。

  • 步骤:

1)每个zk客户端都在 /lock 节点下创建一个有序临时节点 (EPHEMERAL_SEQUENTIAL)。

2)判断创建的节点序号是否最小,如果是最小则获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。

3)当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。

4)取锁成功则执行代码,最后释放锁(删除该节点)。
- 基本锁的java实现demo

引入相关POM依赖

<dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.10</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.8</version>
</dependency>
package com.zpc.zklock;

/**
 * 面向接口编程
 */
public interface Lock {
    /**
     * 获取锁
     */
    public void getLock();

    /**
     * 释放锁
     */
    public void unLock();
}
 
package com.zpc.zklock;

import org.I0Itec.zkclient.ZkClient;

import java.util.concurrent.CountDownLatch;

/**
 * 公共部分抽象出来
 */
public abstract class ZkAbstractLock implements Lock {

    private static final String CONNECTION_STRING = "127.0.0.1:2181";

    /**
     * 分布式环境下,每个应用节点都作为一个zk客户端
     */
    protected ZkClient zkClient = new ZkClient(CONNECTION_STRING);

    /**
     * 在zookeeper中创建的节点名称
     */
    protected static final String PATH = "/lock";

    /**
     * 计数器
     */
    protected CountDownLatch countDownLatch;

    @Override
    public void getLock() {
        if (tryLock()) {
            System.out.println("--------获取锁成功--------");
        } else {
            System.out.println("--------等待锁--------");
            waitLock();
            System.out.println("-------等待线程唤醒--------");
            getLock();
        }
    }

    protected abstract void waitLock();

    protected abstract boolean tryLock();

    @Override
    public void unLock() {
        if (zkClient != null) {
            zkClient.close();
        }
        System.out.println("--------释放锁成功--------");
    }
}

package com.zpc.zklock;

import org.I0Itec.zkclient.IZkDataListener;
import java.util.concurrent.CountDownLatch;

public class ZkDistributeLock extends ZkAbstractLock {

    @Override
    protected void waitLock() {
        //监听zookeeper的lock节点
        IZkDataListener iZkDataListener = new IZkDataListener() {
            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {
            }

            @Override
            public void handleDataDeleted(String dataPath) 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);
    }

    /**
     * 创建临时节点成功即认为获取锁成功
     *
     * @return
     */
    @Override
    protected boolean tryLock() {
        try {
            zkClient.createEphemeral(PATH);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
package com.zpc.zklock;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

/**
 * 订单业务逻辑
 */
public class OrderService implements Runnable {
    CountDownLatch latch;
    /**
     * 搞一个全局变量,以更好地演示效果
     */
    private static  int count=0;

    Lock lock = new ZkDistributeLock();

    public OrderService(CountDownLatch latch){
        this.latch=latch;
    }

    @Override
    public void run() {
        try {
            lock.getLock();
            String orderId = getOrderId();
            System.out.println("订单号:"+orderId);
            for(int i=0;i<999999;i++){
                //模拟耗时长的业务逻辑
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unLock();
            latch.countDown();
        }
    }

    public String getOrderId() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return simpleDateFormat.format(new Date())+"-"+ (++count);
    }
}
package com.zpc.zklock;

import java.util.concurrent.CountDownLatch;

/**
 * 模拟分布式环境下应用程序生成订单号(通过加锁机制保证全局唯一)
 */
public class Client1 {
    private static final int THREADNUM=100;
    public static void main(String[] args) throws Exception {
        //计数器,用来控制主线程挂起,所有线程执行完成后再执行主线程
        CountDownLatch latch=new CountDownLatch(THREADNUM);
        long start = System.currentTimeMillis();
        //开启多个线程模拟分布式环境下多个不同的客户端去获取订单号
        for (int i = 0; i < THREADNUM; i++) {
            new Thread(new OrderService(latch)).start();
        }

        latch.await();
        System.out.println("cost:"+(System.currentTimeMillis()-start));
    }
}
- 优化后的锁的java实现demo
package com.zpc.zklockv2;
import com.zpc.zklock.Lock;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

/**
 * 订单业务逻辑
 */
public class OrderService implements Runnable {
    CountDownLatch latch;
    /**
     * 搞一个全局变量,以更好地演示效果
     */
    private static int count = 0;

    Lock lock;

    public OrderService(CountDownLatch latch, Lock lock) {
        this.latch = latch;
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            lock.getLock();
            //获得锁后的业务逻辑(通常是操作某个共享资源)
            String orderId = getOrderId();
            System.out.println("订单号:" + orderId);
            for(int i=0;i<99999999;i++){
                //模拟耗时的业务逻辑
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unLock();
            latch.countDown();
        }
    }

    public String getOrderId() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return simpleDateFormat.format(new Date()) + "-" + (++count);
    }
}
package com.zpc.zklockv2;

import com.zpc.zklock.Lock;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * 使用zookeeper实现分布式锁
 * 上锁思路:创建临时有序节点,每个上锁的节点均能创建节点成功,只是其序号不同。只有序号最小的可以拥有锁,如果这个节点序号不是最小的则 watch 序号比本身小的前一个节点 (公平有序)。
 * 步骤:
 * 在zookeeper的/lock节点下创建一个有序临时节点(EPHEMERAL_SEQUENTIAL)。
 * 判断当前客户端创建的节点序号是否最小,如果是最小则该客户端获取锁成功。不是则取锁失败,然后 watch 序号比本身小的前一个节点。
 * 当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。
 * 取锁成功则执行代码,最后释放锁(删除该节点)。
 */
public class ZkDistributedLock implements Lock, Watcher {
    private ZooKeeper zk;
    private String root = "/lock";//根
    private String lockName;//竞争资源的标志,lockName中不能包含单词lock
    private String waitNode;//等待前一个锁
    private String myZnode;//当前锁
    private CountDownLatch latch;//计数器
    private int sessionTimeout = 12000;
    private List<Exception> exception = new ArrayList<Exception>();

    public ZkDistributedLock(String config, String lockName) {
        this.lockName = lockName;
        // 创建一个与服务器的连接
        try {
            zk = new ZooKeeper(config, sessionTimeout, this);
            Stat stat = zk.exists(root, false);
            if (stat == null) {
                // 创建根节点(第一个连上服务器的客户端负责创建)
                zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (IOException e) {
            e.printStackTrace();
            exception.add(e);
        } catch (KeeperException e) {
            e.printStackTrace();
            exception.add(e);
        } catch (InterruptedException e) {
            e.printStackTrace();
            exception.add(e);
        }
    }


    @Override
    public void getLock() {
        if (exception.size() > 0) {
            System.out.println("=======exceptions==========" + exception);
            throw new LockException(exception.get(0));
        }
        try {
            if (this.tryLock()) {
                System.out.println("Thread-" + Thread.currentThread().getId() + "-" + myZnode + " 获取锁成功");
                return;
            } else {
                //等待锁
                waitForLock(waitNode, sessionTimeout);
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }

    public boolean tryLock() {
        try {
            String splitStr = "_lock_";
            if (lockName.contains(splitStr)) {
                throw new LockException("lockName can not contains the word 'lock'");
            }
            //创建临时子节点
            myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
            System.out.println("=======================" + myZnode + " is created================== ");
            //取出所有子节点
            List<String> subNodes = zk.getChildren(root, false);
            //取出所有lockName的锁
            List<String> lockObjNodes = new ArrayList<String>();
            for (String node : subNodes) {
                String _node = node.split(splitStr)[0];
                if (_node.equals(lockName)) {
                    lockObjNodes.add(node);
                }
            }
            System.out.println("排序前=====================" + lockObjNodes);
            Collections.sort(lockObjNodes);
            System.out.println("排序后=====================" + lockObjNodes);
            System.out.println(myZnode + "==" + lockObjNodes.get(0));
            if (myZnode.equals(root + "/" + lockObjNodes.get(0))) {
                //如果是最小的节点,则表示取得锁
                System.out.println("--------获取锁成功--------");
                return true;
            }
            //如果不是最小的节点,找到比自己小1的节点
            String myNodeSerialNum = myZnode.substring(myZnode.lastIndexOf("/") + 1);
            System.out.println("myZnode.lastIndexOf=====" + myZnode.lastIndexOf("/"));
            waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, myNodeSerialNum) - 1);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }

    private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
        //判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
        Stat stat = zk.exists(root + "/" + waitNode, true);
        if (stat != null) {
            System.out.println("Thread-" + Thread.currentThread().getId() + " waiting for " + root + "/" + waitNode);
            this.latch = new CountDownLatch(1);
            //阻塞线程,直到latch=0
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);
            this.latch = null;
        }
        return true;
    }

    @Override
    public void unLock() {
        try {
            System.out.println("unlock " + myZnode);
            zk.delete(myZnode, -1);
            myZnode = null;
            if (zk != null) {
                zk.close();
            }
            System.out.println("--------释放锁成功--------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void process(WatchedEvent event) {
        if (this.latch != null) {
            this.latch.countDown();
        }
    }

    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;

        public LockException(String e) {
            super(e);
        }

        public LockException(Exception e) {
            super(e);
        }
    }
}

package com.zpc.zklockv2;
import java.util.concurrent.CountDownLatch;

public class Client2 {
    private static final String CONNECTION_STRING = "127.0.0.1:2181";
    private static final int THREADNUM = 100;

    public static void main(String[] args) throws Exception{
        //计数器,用来控制主线程挂起,所有线程执行完成后再执行主线程
        CountDownLatch mylatch = new CountDownLatch(THREADNUM);
        long start = System.currentTimeMillis();
        //开启多个线程模拟分布式环境下多个不同的客户端去获取订单号
        for (int i = 0; i < THREADNUM; i++) {
            new Thread(new OrderService(mylatch,new ZkDistributedLock(CONNECTION_STRING, "zpcand"))).start();
        }

        mylatch.await();
        System.out.println("cost:" + (System.currentTimeMillis() - start));
    }
}

参考:
1.https://mp.weixin.qq.com/s?__biz=MzA4Njk2NDAzMA==&mid=2660209003&idx=1&sn=390a8d52f7aa404f0bf8e565c7d656c1&chksm=84bb933ab3cc1a2c64c8958de9737ab33d8a6bab8b6372cfc1db2b8e9b021aad7a2ecbf7aa39&scene=0#rd

**欢迎关注公众号**
**微信扫一扫**

猜你喜欢

转载自blog.csdn.net/zpcandzhj/article/details/80383883