一.原理.

zk实现分布式锁主要是使用zk得监听机制来完成得.
这里简单介绍一下zk得Watcher监听机制.
      1.首先它是ZooKeeper的一个核心功能.
      2.watcher是客户端创建的,监听目录节点的数据变化和子目录的变化的
      3.而一旦数据或者子目录状态发生变化,服务器就会通知所有设置在这个目录节点上的watcher.
      4. 那么客户端在下一次心跳时,就可以感知到变化,从而做出相应操作.(客户端是通过心跳与zk维持链接的)
      举例:有一个根节点root,下面有一个子节点node1,node2,等,比如客户端client1在root上创建一个子节点变更的watcher监听的话,
然后删除了node1,发生了变化,然后客户端在下一次心跳时就可以感知到这个变化,执行相关操作.

二.步骤.

     zk分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新枷锁。

具体步骤:

  1. 假如有三个客户端a,b,c尝试获取锁,每个客户端需要做得就是尝试创建同样路径得临时节点(临时节点得特性是,客户端通过心跳和zk维持链接,如果客户端挡掉了,链接消失,那么对应的临时节点也会消失.)
  2. 因为节点 具有唯一性,所以a,b,c只能有一个创建成功,这里假如a创建成功,那么a就获取到了这个锁,b和c就需要注册一个watch监听;
  3. 如果a获取到锁在执行也业务逻辑得过程中挡掉了,那么因为是临时节点,所以服务端会自动删除这个临时节点,同时通知b和c,如果a正常执行完,将这个临时节点进行删除,服务端也会感知到,通知b和c进行重新抢锁.

三.简单代码

/**
 * ZooKeeperSession
 * @author Administrator
 *
 */
public class ZooKeeperSession {
	
	private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
	
	private ZooKeeper zookeeper;
	private CountDownLatch latch;

	public ZooKeeperSession() {
		try {
			this.zookeeper = new ZooKeeper(
					"192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 
					50000, 
					new ZooKeeperWatcher());			
			try {
				connectedSemaphore.await();
			} catch(InterruptedException e) {
				e.printStackTrace();
			}

			System.out.println("ZooKeeper session established......");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 获取分布式锁
	 * @param productId
	 */
	public Boolean acquireDistributedLock(Long productId) {
		String path = "/product-lock-" + productId;
	
		try {
			zookeeper.create(path, "".getBytes(), 
					Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
			return true;
		} catch (Exception e) {
			while(true) {
				try {
			Stat stat = zk.exists(path, true); // 相当于是给node注册一个监听器,去看看这个监听器是否存在
			if(stat != null) {
				this.latch = new CountDownLatch(1);
				this.latch.await(waitTime, TimeUnit.MILLISECONDS);
				this.latch = null;
				}
				zookeeper.create(path, "".getBytes(), 
										Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
				return true;
				} catch(Exception e) {
				continue;
				}
			}
		}
		return true;
	}
	
	/**
	 * 释放掉一个分布式锁
	 * @param productId
	 */
	public void releaseDistributedLock(Long productId) {
		String path = "/product-lock-" + productId;
		try {
			zookeeper.delete(path, -1); 
			System.out.println("release the lock for product[id=" + productId + "]......");  
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * 建立zk session的watcher
	 * @author Administrator
	 *
	 */
	private class ZooKeeperWatcher implements Watcher {

		public void process(WatchedEvent event) {
			System.out.println("Receive watched event: " + event.getState());

			if(KeeperState.SyncConnected == event.getState()) {
				connectedSemaphore.countDown();
			} 

			if(this.latch != null) {  
			this.latch.countDown();  
			}
		}
		
	}
	
	/**
	 * 封装单例的静态内部类
	 * @author Administrator
	 *
	 */
	private static class Singleton {
		
		private static ZooKeeperSession instance;
		
		static {
			instance = new ZooKeeperSession();
		}
		
		public static ZooKeeperSession getInstance() {
			return instance;
		}
		
	}
	
	/**
	 * 获取单例
	 * @return
	 */
	public static ZooKeeperSession getInstance() {
		return Singleton.getInstance();
	}
	
	/**
	 * 初始化单例的便捷方法
	 */
	public static void init() {
		getInstance();
	}
	
}

四.redis分布式锁和zk分布式锁的对比

redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能

zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小

另外一点就是,如果是redis获取锁的那个客户端bug了或者挂了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁

redis分布式锁大家每发现好麻烦吗?遍历上锁,计算时间等等。。。zk的分布式锁语义清晰实现简单

所以先不分析太多的东西,就说这两点,我个人实践认为zk的分布式锁比redis的分布式锁牢靠、而且模型简单易用