基于ZooKeeper实现分布式锁(公平和非公平两种形式)

关于ZooKeeper,上一篇博客有更详细的介绍(传送门)。

一、分布式锁

在同一个jvm进程中时,可以使用JUC提供的一些锁来解决多个线程竞争同一个共享资源时候的线程安全问题,但是当多个不同机器上的不同jvm进程共同竞争同一个共享资源时候,juc包的锁就无能无力了,这时候就需要分布式锁了。 

常见的分布式锁实现方式有Redis的SETNX和GETSET函数,数据库锁,zk的数据节点和监听事件等。

其中Redis版本的实现之前已有博客介绍(传送门),现在就谈一下使用ZooKeeper实现的方案。

二、实现原理

在zk中是使用文件目录的格式(或者叫树形结构)存放节点内容,其中节点类型分为:

  • 持久节点(PERSISTENT ):节点创建后,一直存在,直到主动删除了该节点。
  • 临时节点(EPHEMERAL):生命周期和客户端会话绑定,一旦客户端会话失效,这个节点就会自动删除。
  • 序列节点(SEQUENTIAL ):多个线程创建同一个顺序节点时候,每个线程会得到一个带有编号的节点,节点编号是递增不重复的,如下图:image.png

具体在节点创建过程中,可以混合使用,比如临时顺序节点(EPHEMERAL_SEQUENTIAL)。

由此,我们可以得到两种实现方案。

第一种: 公平方式

  • 创建临时顺序节点,比如/root/node,假设返回结果为nodeId(node0000000001、node0000000002... ...)。
  • 获取/root下所有子节点,用自己创建的nodeId的序号与所有子节点比较,看看自己是不是编号最小的。如果是最小的则就相当于获取到了锁,如果自己不是最小的,就从所有子节点里面获取比自己小一号的那个节点,设置监听该节点的事件,然后挂起当前线程。
  • 当最小编号的线程获取锁,处理完业务后删除自己对应的nodeId(释放锁),触发节点删除监听事件从而会激活比自己大一号的节点的线程从阻塞变为运行状态,被激活的线程应该就是当前node序列号最小的了,然后就会获取到锁。

这种方式的特点是,线程获得锁的先后顺序跟创建节点的先后顺序保持了一致,所以我称为公平模式。还有就是,不存在锁释放之后多线程争抢的问题,性能会更好。

第二种、非公平方式

  • 假设有一个应用由n个进程组成,这些进程尝试获取一个锁。再次强调,ZooKeeper并未直接暴露原语,因此我们使用ZooKeeper的接口来管理znode,以此来实现锁。为了获得一个锁,每个进程p尝试创建znode,名为/lock。如果进程p成功创建了znode,就表示它获得了锁并可以继续执行其临界区域的代码。不过一个潜在的问题是进程p可能崩溃,导致这个锁永远无法释放。在这种情况下,没有任何其他进程可以再次获得这个锁,整个系统可能因死锁而失灵。为了避免这种情况,我们不得不在创建这个节点时指定/lock为临时节点。其他进程因znode存在而创建/lock失败。因此,进程监听/lock的变化,并在检测到/lock删除时再次尝试创建节点来获得锁。当收到/lock删除的通知时,如果进程p还需要继续获取锁,它就继续尝试创建/lock的步骤,如果其他进程已经创建了,就继续监听节点。

这种方式的特点是,当第一个获得锁的线程释放锁之后,其它在等待的所有线程会一起去争抢这把锁,不存在固定的先后顺序。

三、放码过来

1、Maven依赖

        <dependency>
  			<groupId>org.apache.zookeeper</groupId>
  			<artifactId>zookeeper</artifactId>
  			<version>3.4.13</version>
		</dependency>

2、ZookeeperDistributedLock

因为同时提供公平和非公平两种方式的实现,所以在类中定义了两个子内部类,各自实现不同的lock和unlock方法。外层类提供了根据参数创建不同子类对象的静态方法。

另一个内部类LockWatcher的设计也很重要:构造函数传入初始值为1的CountDownLatch,监听NodeDelete事件,被触发时倒计数锁存器减一让线程由等待状态继续往下执行。

/**
 * zk分布式锁
 * @author z_hh
 * @time 2018年12月31日
 */
public class ZookeeperDistributedLock {

	// zk客户端
    private ZooKeeper zk;
    // zk是一个目录结构,root为最外层目录
    private String root = "/locks";
    // 用来同步等待zkclient链接到了服务端
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    // 会话超时时间:毫秒
    private final static int sessionTimeout = 3000;
    // 节点数据:无需数据
    private final static byte[] data= new byte[0];

    /**
     * 创建一个zk分布式锁实例
     * @param config zk连接字符串
     * @param lockName 锁名称
     * @param isFair 是否公平
     * @return 公平 or 非公平锁对象
     */
    public static ZookeeperDistributedLock create(String config, String lockName, boolean isFair) {
    	return isFair ? new ZookeeperDistributedLock(config, lockName).new FairLock(config, lockName)
    			: new ZookeeperDistributedLock(config, lockName).new UnFairLock(config, lockName);
    }

    // 构造函数私有化
    private ZookeeperDistributedLock(String config, String lockName) {
        try {
            zk = new ZooKeeper(config, sessionTimeout, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    // 建立连接
                    if (event.getState() == KeeperState.SyncConnected) {
                        connectedSignal.countDown();
                    }
                }
            });
            // 等待连接建立完毕
            connectedSignal.await();
            Stat stat = zk.exists(root, false);
            if (null == stat) {
                // 创建根节点
                zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 锁
     * @throws Exception
     */
    public void lock() throws Exception {
    	// 具体子类实现
    	throw new UnsupportedOperationException("不支持的操作!");
    }

    /**
     * 解锁
     */
    public void unlock() {
    	// 具体子类实现
    	throw new UnsupportedOperationException("不支持的操作!");
    }

    // 监听器:一旦锁被释放,从等待状态唤醒继续往下执行
    private class LockWatcher implements Watcher {
        private CountDownLatch latch = null;

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

        @Override
        public void process(WatchedEvent event) {

            if (event.getType() == Event.EventType.NodeDeleted)
                latch.countDown();
        }
    }

    // 公平锁实现
	private class FairLock extends ZookeeperDistributedLock {
		//锁的名称
	    private String lockName;
	    //当前线程创建的序列node
	    private ThreadLocal<String> nodeId = new ThreadLocal<>();
	    
		public FairLock(String config, String lockName) {
			super(config, lockName);
			this.lockName = lockName;
		}

		@Override
		public void lock() throws Exception {
	        try {
	            // 创建临时子节点,含序列
	            String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
	                    CreateMode.EPHEMERAL_SEQUENTIAL);

	            System.out.println(Thread.currentThread().getName() + myNode + "created");

	            // 取出所有子节点
	            List<String> subNodes = zk.getChildren(root, false);
	            TreeSet<String> sortedNodes = new TreeSet<>();// 字典序
	            for(String node :subNodes) {
	                sortedNodes.add(root +"/" +node);
	            }
	            
	            String smallNode = sortedNodes.first();// 最小,即第一个创建的
	            String preNode = sortedNodes.lower(myNode);// 前一个

	            if (myNode.equals( smallNode)) {
	                // 如果是最小的节点,则表示取得锁
	                System.out.println(Thread.currentThread().getName() + myNode + "get lock");
	                this.nodeId.set(myNode);
	                return;
	            }

	            CountDownLatch latch = new CountDownLatch(1);
	            Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同时注册监听。
	            // 判断比自己小一个数的节点是否存在,如果存在等待锁,同时注册监听
	            if (stat != null) {
	                System.out.println(Thread.currentThread().getName() + myNode +
	                        " waiting for " + root + "/" + preNode + " released lock");

	                latch.await();// 等待,这里应该一直等待其他线程释放锁
	                nodeId.set(myNode);
	                latch = null;
	            }
	            // 不存在,说明锁释放了,轮到自己了
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }

	    }

		@Override
	    public void unlock() {
	        try {
	            System.out.println(Thread.currentThread().getName() + nodeId.get() + "unlock ");
	            if (null != nodeId) {
	                zk.delete(nodeId.get(), -1);// 删除节点
	            }
	            nodeId.remove();
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        } catch (KeeperException e) {
	            e.printStackTrace();
	        }
	    }
	}

	// 非公平锁实现
	private class UnFairLock extends ZookeeperDistributedLock {
		// 锁的全路径
	    private String lockPath;
	    
		public UnFairLock(String config, String lockName) {
			super(config, lockName);
			lockPath = root + "/" + Objects.requireNonNull(lockName, "lockName不能为null!");
		}

		@Override
		public void lock() throws Exception {
			while (true) {
	    		try {
	    			zk.create(lockPath , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
	                        CreateMode.EPHEMERAL);
	    			 System.out.println(Thread.currentThread().getName() + "get lock");
	    			 return;
	    		} catch (NodeExistsException e) {// 说明锁被占用,注册监听并等待
	    			CountDownLatch latch = new CountDownLatch(1);
	                Stat stat = zk.exists(lockPath, new LockWatcher(latch));// 注册监听。
	                if (stat != null) {
	                    System.out.println(Thread.currentThread().getName() +
	                            " waiting for " + lockPath + " released lock");

	                    latch.await();// 等待
	                    latch = null;
	                }
				} catch (Exception e) {
					throw e;
				}
			}
	    }

		@Override
	    public void unlock() {
	    	try {
	            System.out.println(Thread.currentThread().getName() + "unlock ");
	            zk.delete(lockPath, -1);// 删除节点
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        } catch (KeeperException e) {
	            e.printStackTrace();
	        }
	    }
	}

}

3、测试代码

分别测试了两种方式的分布式锁。代码中使用CountDownLatch、Thread.sleep、parallelStream尽量模拟了真实场景。

public class ZkLockTest {
	
	// zk地址
	private String CONNECT_STRING = "xxoo:2181";
	
	// 公平
	@Test
	public void testFair() throws Throwable {
		test(true);
	}
	
	// 非公平
	@Test
	public void testUnFair() throws Throwable {
		test(false);
	}
	
	public void test(boolean isFair) throws Exception {
		List<Thread> threads = new ArrayList<>();
		// 线程数
		int threadCount = 10;
		// 等待所有线程执行完毕
		CountDownLatch latch = new CountDownLatch(threadCount);
		// 准备线程及执行任务
		for (int i = 0; i < threadCount; i++) {
			threads.add(new Thread(() -> {
				ZookeeperDistributedLock zkLock = ZookeeperDistributedLock.create(CONNECT_STRING, "zhh_lock", isFair);
				try {
					// 抢锁
					zkLock.lock();
					// 模拟做事
					doSomething(1);
					// 解锁
					zkLock.unlock();
					latch.countDown();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}, "thread-" + i));
		}
		// 并行
		threads.parallelStream()
			.forEach(Thread::start);
		// 等待所有线程执行完毕
		latch.await();
	}
	
	// 模拟做一些事情
	private void doSomething(int second) {
		try {
			System.out.println(Thread.currentThread().getName() + "获得了锁,开始执行任务!");
			Thread.sleep(second * 1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

4、测试日志

第一种,公平方式。

第二种,非公平方式。

 

?

2018就到此了,来年再干,预祝大家2019万事如意!!!

在2018跌倒了,就在2019站起来!!!

 

 

 

 

 

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值