分布式锁-使用Zookeeper实现分布式锁

#使用Zookeeper实现分布式锁
目前,分布式应用渐渐取代了传统的单体应用,也有越来越多的程序员投入到分布式应用开发中。既然是分布式开发,肯定会遇到共享资源的访问(修改)问题。单体应用中多线程访问(修改)共享资源,我们想到的解决方案是synchronized、ReentrantLock,以保证数据的安全性和一致性。那么问题来了,分布式应用如何实现分布式锁呢?

好慌,打开浏览器百度一波,我们得到的答案如下:

  1. 基于数据库实现分布式锁
  2. 基于Redis实现分布式锁
  3. 基于ZooKeeper实现分布式锁

开心,原来分布式锁的实现已经有了成熟的解决方案。那么,我们需要做的就是用代码去实现这些方案。笔主在项目开发过程中,也遇到过共享资源的访问(修改)问题。当时为了快速解决这个问题,选用了第二种方案,基于Redis实现分布式锁。为什么选用了Redis?理由如下:

  1. 生产环境中已有基于Redis实现分布式锁的案例
  2. 考虑到Redis单线程,顺序处理命令的特性

闲下来的时候,还是想用不同的方案去解决同一个问题。那么本篇文章的主题来了,那就是使用ZooKeeper实现分布式锁。

什么是ZooKeeper?

这里直接借用百度百科对ZooKeeper下的定义。

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

简单理解,ZooKeeper将复杂的服务封装为简单的API,ZooKeeper集群保证了其可以提供稳定的服务。

那么,如何基于ZooKeeper实现分布式锁呢?思路如下:

  1. 在ZooKeeper的实例上创建一个永久节点作为根节点;
  2. 在根节点下创建一系列的临时顺序节点;
  3. 当某个临时顺序节点需要访问(修改)共享资源时,要求该节点获取到锁(其实就是访问共享资源的资格)?那么如何判定该节点能不能获取到锁呢?判定思路如下:
    3.1. 获取根节点下的所有临时顺序节点,对所有临时顺序节点排序。
    3.2. 如果当前节点是最小的那个临时顺序节点,就获取到锁。
    3.2.如果当前节点不是最小的那个临时顺序节点,那么就监听当前节点的前一个节点的删除事件,当前节点的前一个节点删除时,再去竞争锁。

这个实现思路主要依赖了ZooKeeper的哪些性质呢?

  1. ZooKeeper可以创建节点,并且节点有四种类型PERSISTENT(永久)、PERSISTENT_SEQUENTIAL(永久有序)、EPHEMERAL(临时)、EPHEMERAL_SEQUENTIAL(临时有序)。
package org.apache.zookeeper;
/***
 2.  CreateMode value determines how the znode is created on ZooKeeper.
 */
public enum CreateMode {
    
    PERSISTENT (0, false, false),
  
    PERSISTENT_SEQUENTIAL (2, false, true),
    
    EPHEMERAL (1, true, false),
   
    EPHEMERAL_SEQUENTIAL (3, true, true);
}

2.ZooKeeper的事件监听特性。当ZooKeeper客户端访问节点时,可以对节点设置事件监听,当节点创建、删除、数据变化、子节点变化时,会通知监听的客户端。

/**
 1. Enumeration of types of events that may occur on the ZooKeeper
  */
 public enum EventType {
            None (-1),
            NodeCreated (1),
            NodeDeleted (2),
            NodeDataChanged (3),
            NodeChildrenChanged (4);
 }

吧啦吧啦讲了这么多,现在上具体的实现代码。

package com.example.demo1.distribute.lock;

import java.io.IOException;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;

public class ZKClient {

	private static ZooKeeper instance = null;
	//连接地址
	private final static String CONNECT_STRING ="127.0.0.1:2181";
	//会话超时时间
	private final static int SESSION_TIMEOUT = 3000;
	//私有的构造方法
	private ZKClient () {}
	
	/**
	 * 获取连接实例
	 * @return
	 */
	public static ZooKeeper getInstance() {
		if (instance == null) {
			synchronized (ZKClient.class) {
				if (instance == null) {
					try {
						instance = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, new Watcher(){
							@Override
							public void process(WatchedEvent event) {
								
							}
						});
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}
		return instance;
	}
}

ZKClient这个类便于获取Zookeeper的连接实例,也就是客户端。使用单例模式实现,在分布式应用中,每一个应用使用一个客户端和ZooKeeper服务交互即可。

package com.example.demo1.distribute.lock;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * ZK实现分布式锁
 * @author zhaoheng
 * @date   2018年8月3日
 */
public class DistributeLock {
	
	private static final Logger LOG = LoggerFactory.getLogger(DistributeLock.class);
	
	//根节点
	private static final String ROOT_NODE_LOCK = "/ROOT_LOCK";
	
	private ZooKeeper zooKeeper;
	
	//当前节点锁的id
	private String currentLockId;
	
	//节点存放的数据
	private final static String DATA = "node";
	
	private final CountDownLatch countDownLatch = new CountDownLatch(1);
	
	public DistributeLock () {
		//初始化连接实例
		this.zooKeeper = ZKClient.getInstance();
	}
	
	/**
	 * 检查根节点
	 */
	public void checkRootNode () {
		try {
			//先判断根节点是否存在
			Stat stat = zooKeeper.exists(ROOT_NODE_LOCK, false);
			if (stat == null) {
				//根节点不存在则创建根节点
				zooKeeper.create(ROOT_NODE_LOCK, DATA.getBytes(), 
						ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
			}
		} catch (Exception e) {
			LOG.error("创建根节点出现异常:", e);
		}
	}
	
	/***
	 * 获取分布式锁
	 * @return
	 */
	public synchronized boolean lock () {
		try {
			//在根节点下创建临时顺序节点
			currentLockId = zooKeeper.create(ROOT_NODE_LOCK + "/", DATA.getBytes(), 
					ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
			LOG.info("线程{}创建了节点{}", Thread.currentThread().getName(), currentLockId);
			//获取根节点下的子节点集合
			List childNodeList = zooKeeper.getChildren(ROOT_NODE_LOCK, true);
			//子节点集合排序
			Collections.sort(childNodeList);
			//获取到第一个节点
			String firstNode = childNodeList.get(0);
			String currentNode = currentLockId.substring(currentLockId.lastIndexOf("/") + 1);
			//如果当前子节点就是第一个子节点
			if (currentNode.equals(firstNode)) {
				LOG.info("线程{}获取到分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
				return true;
			}
			int index = childNodeList.indexOf(currentNode);
			LOG.info("线程{}对应的子节点不是第一个节点,当前子节点{}", Thread.currentThread().getName(), currentLockId);
			if (index > 0) {
				//获取当前子节点的前一个节点
				String preNode = childNodeList.get(index - 1);
				LOG.info("线程{}获取当前子节点的前一个节点,当前子节点{},前一个节点{}", Thread.currentThread().getName() , currentLockId, preNode);
				zooKeeper.exists(ROOT_NODE_LOCK + "/" +preNode, new Watcher (){

					@Override
					public void process(WatchedEvent event) {
						//监听前一个节点的删除事件
						if (event.getType().equals(EventType.NodeDeleted)) {
							LOG.info("当前节点{}监听到前一个节点{}删除事件", currentLockId, preNode);
							countDownLatch.countDown();
						}
					}
					
				});
				countDownLatch.await();
				LOG.info("线程{}获取到分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
				return true;
			}
		} catch (Exception e) {
			LOG.error("获取分布式锁出现异常:", e);
		}
		return false;
	}
	
	/**
	 * 释放分布式锁
	 * @return
	 */
	public synchronized boolean unlock () {
		try {
			LOG.info("线程{}开始释放分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
			//删除当前子节点
			zooKeeper.delete(currentLockId, -1);
			LOG.info("线程{}释放分布式锁成功,当前子节点{}", Thread.currentThread().getName(), currentLockId);
			return true;
		} catch (Exception e) {
			LOG.error("释放分布式锁出现异常:", e);
		}
		return false;
	}
}

DistributeLock类是基于ZooKeeper的具体实现,采用的就是刚才讨论的思路。

/**
 * 测试分布式锁
 * @author zhaoheng
 * @date   2018年8月8日
 */
public class TestDistributeLock {

	private static final Logger LOG = LoggerFactory.getLogger(TestDistributeLock.class);
	
	public static void main(String[] args) {
		CountDownLatch countDownLatch = new CountDownLatch(10);
		//定义一个定长的线程池
		ExecutorService executorService = Executors.newFixedThreadPool(10);
		Random random = new Random();
		for (int i = 0; i < 10; i++) {
			executorService.execute( new Runnable() {
				DistributeLock distributeLock = null;
				@Override
				public void run() {
					try {
						distributeLock = new DistributeLock();
						//检查根节点
						distributeLock.checkRootNode();
						countDownLatch.countDown();
						countDownLatch.await();
						//获取锁
						distributeLock.lock();
						//随机睡眠一段时间,模拟业务处理
						Thread.sleep(random.nextInt(1000));
					} catch (Exception e) {
						LOG.error("处理业务出现异常:", e);
					} finally {
						if (distributeLock != null) {
							//释放锁
							distributeLock.unlock();
						}
					}
				}
				
			});
		}
	}
}

当然,少不了测试类。在测试类中,使用10个线程模拟竞争分布式锁。另外,也使用了Jdk并发包下的工具类CountDownLatch,10个线程并行执行。

2018-08-08 17:23:56.059 - 线程pool-2-thread-1创建了节点/ROOT_LOCK/0000000000
2018-08-08 17:23:56.091 - 线程pool-2-thread-8创建了节点/ROOT_LOCK/0000000001
2018-08-08 17:23:56.091 - 线程pool-2-thread-10创建了节点/ROOT_LOCK/0000000002
2018-08-08 17:23:56.092 - 线程pool-2-thread-2创建了节点/ROOT_LOCK/0000000003
2018-08-08 17:23:56.092 - 线程pool-2-thread-9创建了节点/ROOT_LOCK/0000000004
2018-08-08 17:23:56.093 - 线程pool-2-thread-4创建了节点/ROOT_LOCK/0000000005
2018-08-08 17:23:56.093 - 线程pool-2-thread-3创建了节点/ROOT_LOCK/0000000006
2018-08-08 17:23:56.093 - 线程pool-2-thread-7创建了节点/ROOT_LOCK/0000000007
2018-08-08 17:23:56.094 - 线程pool-2-thread-6创建了节点/ROOT_LOCK/0000000008
2018-08-08 17:23:56.097 - 线程pool-2-thread-5创建了节点/ROOT_LOCK/0000000009
2018-08-08 17:23:56.101 - 线程pool-2-thread-1获取到分布式锁,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:56.101 - 线程pool-2-thread-8对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:56.101 - 线程pool-2-thread-8获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000001,前一个节点0000000000
2018-08-08 17:23:56.101 - 线程pool-2-thread-10对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:56.101 - 线程pool-2-thread-10获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000002,前一个节点0000000001
2018-08-08 17:23:56.102 - 线程pool-2-thread-2对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:56.103 - 线程pool-2-thread-2获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000003,前一个节点0000000002
2018-08-08 17:23:56.103 - 线程pool-2-thread-3对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:23:56.103 - 线程pool-2-thread-3获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000006,前一个节点0000000005
2018-08-08 17:23:56.103 - 线程pool-2-thread-7对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:23:56.104 - 线程pool-2-thread-7获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000007,前一个节点0000000006
2018-08-08 17:23:56.103 - 线程pool-2-thread-4对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:56.105 - 线程pool-2-thread-4获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000005,前一个节点0000000004
2018-08-08 17:23:56.103 - 线程pool-2-thread-9对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:56.107 - 线程pool-2-thread-9获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000004,前一个节点0000000003
2018-08-08 17:23:56.109 - 线程pool-2-thread-5对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:23:56.109 - 线程pool-2-thread-5获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000009,前一个节点0000000008
2018-08-08 17:23:56.109 - 线程pool-2-thread-6对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:23:56.112 - 线程pool-2-thread-6获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000008,前一个节点0000000007
2018-08-08 17:23:57.059 - 线程pool-2-thread-1开始释放分布式锁,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:57.237 - 线程pool-2-thread-1释放分布式锁成功,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:57.237 - 当前节点/ROOT_LOCK/0000000001监听到前一个节点0000000000删除事件
2018-08-08 17:23:57.238 - 线程pool-2-thread-8获取到分布式锁,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.701 - 线程pool-2-thread-8开始释放分布式锁,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.844 - 当前节点/ROOT_LOCK/0000000002监听到前一个节点0000000001删除事件
2018-08-08 17:23:57.844 - 线程pool-2-thread-8释放分布式锁成功,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.844 - 线程pool-2-thread-10获取到分布式锁,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.032 - 线程pool-2-thread-10开始释放分布式锁,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.256 - 当前节点/ROOT_LOCK/0000000003监听到前一个节点0000000002删除事件
2018-08-08 17:23:58.257 - 线程pool-2-thread-10释放分布式锁成功,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.257 - 线程pool-2-thread-2获取到分布式锁,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:58.868 - 线程pool-2-thread-2开始释放分布式锁,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:59.119 - 当前节点/ROOT_LOCK/0000000004监听到前一个节点0000000003删除事件
2018-08-08 17:23:59.120 - 线程pool-2-thread-9获取到分布式锁,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.120 - 线程pool-2-thread-2释放分布式锁成功,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:59.230 - 线程pool-2-thread-9开始释放分布式锁,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.286 - 当前节点/ROOT_LOCK/0000000005监听到前一个节点0000000004删除事件
2018-08-08 17:23:59.286 - 线程pool-2-thread-9释放分布式锁成功,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.286 - 线程pool-2-thread-4获取到分布式锁,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.641 - 线程pool-2-thread-4开始释放分布式锁,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.675 - 当前节点/ROOT_LOCK/0000000006监听到前一个节点0000000005删除事件
2018-08-08 17:23:59.675 - 线程pool-2-thread-4释放分布式锁成功,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.676 - 线程pool-2-thread-3获取到分布式锁,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.214 - 线程pool-2-thread-3开始释放分布式锁,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.299 - 当前节点/ROOT_LOCK/0000000007监听到前一个节点0000000006删除事件
2018-08-08 17:24:00.299 - 线程pool-2-thread-3释放分布式锁成功,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.299 - 线程pool-2-thread-7获取到分布式锁,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.365 - 线程pool-2-thread-7开始释放分布式锁,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.428 - 当前节点/ROOT_LOCK/0000000008监听到前一个节点0000000007删除事件
2018-08-08 17:24:00.429 - 线程pool-2-thread-7释放分布式锁成功,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.429 - 线程pool-2-thread-6获取到分布式锁,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.538 - 线程pool-2-thread-6开始释放分布式锁,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.564 - 当前节点/ROOT_LOCK/0000000009监听到前一个节点0000000008删除事件
2018-08-08 17:24:00.564 - 线程pool-2-thread-6释放分布式锁成功,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.564 - 线程pool-2-thread-5获取到分布式锁,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:24:00.646 - 线程pool-2-thread-5开始释放分布式锁,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:24:00.674 - 线程pool-2-thread-5释放分布式锁成功,当前子节点/ROOT_LOCK/0000000009

观察测试结果,节点/ROOT_LOCK/0000000000是最小的临时顺序节点,该节点创建成功后就获取到了分布式锁。其他临时顺序节点则监听其前一个节点的删除事件,其前一个节点删除时,就去竞争分布式锁。
测试过程中也发现,10个线程并行竞争分布式锁时,执行checkRootNode()方法会有9个线程捕获到NodeExistsException,这是因为第一个线程创建根节点成功后,其他根节点再去创建根节点就失败了。

2018-08-08 17:23:55.993 - 创建根节点出现异常:
org.apache.zookeeper.KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /ROOT_LOCK
	at org.apache.zookeeper.KeeperException.create(KeeperException.java:119)
	at org.apache.zookeeper.KeeperException.create(KeeperException.java:51)
	at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:783)
	at com.example.demo1.distribute.lock.DistributeLock.checkRootNode(DistributeLock.java:57)
	at com.example.demo1.distribute.lock.TestDistributeLock$1.run(TestDistributeLock.java:32)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
2018-08-08 17:23:55.993 - 创建根节点出现异常:

至此,基于ZooKeeper实现分布式锁介绍完毕。笔主水平有限,笔误或者不当之处还请批评指正。

你可能感兴趣的:(分布式)