Zookeeper 分布式锁

  假设有如下场景,在一个分布式环境,由A, B, C,服务3个服务组成,服务有一个编辑修改,A B个服务同时打开表单,修改某一条数据。那此刻A B 三个服务看到的都是一样的没有修改的数据,如果A先提交,那么B由于不知道A已经修改过数据,B认为数据没有修改也提交,导致A的数据被覆盖。可以知道要想A改的数据能被B知道,那么打开表单和保存修改是同一个原子,在A打开表单的时候获取锁,锁住查询接口,禁止其他服务访问,等A保存完成B才能获取查询接口并打开表单,那就是要同步。这个例子可能不怎么恰当,这个场景加锁服务性能将会变得很拥堵,在这只是举一个例子,现实中获取锁应该是处理某个相同的任务。
  在一个服务下实现同步,由于是一个进程,通一个进程下实现同步那就是多线程同步,网上多线程同步讲解比较多,这里略过。那么在分布式环境实现同步可以通过zookeeper去实现同步。

  在上图中,我们通过一个zookeeper集群去创建相同的临时节点,由于zookeeper节点唯一性,如果存在,再创建就会抛出异常,那么服务1创建节点成功的服务获取节点锁,其他服务创建节点锁失败,等待获取锁。当服务1任务完成,删除节点。其他两个服务可以监听节点变化,当节点删除,触发watch机制,这两个服务再次抢占创建继续去创建节点获取锁,如此循环。为什么上面使用临时节点,假如服务1在执行任务的过程中挂掉,无法删除节点,那么节点就一直存在,监听机制永远不会触发,导致其他服务获取不了锁。当使用临时节点,如果服务挂掉,zookeeper会在会话失效之后自动删除节点,那么其他服务就可以继续获取锁了。
  不过上面这种思路还是有缺陷,当服务多了之后,节点删除之后那么就会通知所有的服务,将会加大系统开销。这时候可以使用临时时序节点,当创建相同的时序节点,时序节点在节点名称后面追加有序标识,先创建的标识在较小,后创建的标识较大。这时候后面创建的节点监听他前面创建的节点,当他前面节点删除了,那么只会触发他后面一个节点的监听事件。而不会像上面那种所有的服务都会通知,提高服务性能。
  打开zookeeper可视化工具,通过代码创建几个时序节点,看看什么时序节点是什么样的,如下图:
在这里插入图片描述
  在lockNode节点下创建node的时序节点,其排列还是一个递增序列,与创建的顺序一样。
  通过上面分析,可以得出下面程序的流程图
在这里插入图片描述
  这个流程里面如果是最小节点,那比较简单,复杂就在不是最小节点,当不是最小节点,由于节点的创建具有有序性,先创建的小,后创建的大,那么监听在他前面创建的一个节点就可以,(将获取的节点树排序,倒数第二个就是他的上一个节点,倒数第一个就是他自己)。
  根据上面的逻辑图,实现的代码如下

package com.work;

import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.server.ZooKeeperServer;

public class Client1 {
	
	//创建节点
	public static void createNode(ZooKeeper zookeeper) throws Exception{
		//当前服务创建的时序节点节点永远是最大的
		String path = zookeeper.create("/lockNode/node", "服务1".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//		System.out.println(getTimes() + "...." + "服务1创建节点成功节点为" + path);
	    getWatchNode(zookeeper, path);
	}
	
	
    //获取比当前节点小的节点,并获取节点说
	public static void getWatchNode(ZooKeeper zookeeper, String path) throws Exception{
		List<String> childNode = zookeeper.getChildren("/lockNode", null);
		Collections.sort(childNode);//将获取的时序节点排序
		
		//当前服务创建的时序节点节点永远是最大的,因此它的上一个就是比他小的节点
		if(!childNode.get(0).equals(path.subSequence(path.lastIndexOf("/") + 1, path.length()))){//当前节点不是最小节点
			
			
			
			String node = childNode.get(childNode.size() - 2);//获取倒数第二个节点,最为本级节点的监听节点;本级是倒数第一个
			byte[] data = zookeeper.getData("/lockNode/" + node, null, new Stat());
			System.out.println(getTimes() + "...." + "判断服务1创建的节点" + path + "不是最小节点, 开始监听" + new String(data) + "的节点" + node);
			//监听倒数第二个节点
			doprocess(zookeeper, node, path);
			
			
		}else{//本级节点就是最小节点,获取节点锁
			byte[] data = zookeeper.getData(path, null, new Stat());
			System.out.println(getTimes() + "....判断" + new String(data) + "创建的节点" + path + "是最小节点, 开始处理自己的事物");
			doWork(zookeeper, path);
			
		}
	
	}

	//注册节点监听事件
	public static void doprocess(final ZooKeeper zookeeper, final String pathWatch, final String node) throws Exception{
		final List<String> param = new ArrayList<String>();
		Watcher watch = new Watcher(){

			@Override
			public void process(WatchedEvent event) {
				if(event.getType() == EventType.NodeDeleted){//如果是节点删除事件,获取锁
				
					System.out.println(getTimes() + "...." + "监听节" + pathWatch + "事件已经触发,服务1获取锁处理自己的事物");
					param.add("delete");
					doWork(zookeeper, node);
					
				}
				
			}
			
		};
		
		
		try{
			//注册节点监听器
			zookeeper.getData("/lockNode/" + pathWatch, watch, new Stat());
		}catch(NoNodeException e){//如果在监听节点的时候节点删除,触发节点不存在异常,捕获异常直接调用doWork,因为本级节点直接是最小节点
			doWork(zookeeper, node);
		}
		

	}
	
	public static String getTimes(){
		Calendar calendar = Calendar.getInstance();
		String hour = String.valueOf(calendar.get(Calendar.HOUR));
		String minute = String.valueOf(calendar.get(Calendar.MINUTE));
		String second = String.valueOf(calendar.get(Calendar.SECOND));
		String MILLISECOND = String.valueOf(calendar.get(Calendar.MILLISECOND));
		return hour + "时" + minute + "分" + second + "秒" + MILLISECOND + "毫秒";
	}
	
	public static void doWork(ZooKeeper zookeeper, String path){
		
		try {
			Thread.sleep(getSleepTimes());
			
			
			
			zookeeper.delete(path, 0);
			System.out.println(getTimes() + "...." + "服务1事物处理完成,删除节点" + path + "释放锁");
			
	
			
			Thread.sleep(getSleepTimes());//等待5秒再去创建节点获取锁
			
			//创建节点,去获取锁
			createNode(zookeeper);
			
			
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static long getSleepTimes(){
        DecimalFormat de = new DecimalFormat("####");
		
		return Long.parseLong(de.format(Math.random() * 10000));
	}
	
	public static void main(String[] args) throws Exception{
		
		ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183", 1000, null);
		
		createNode(zookeeper);
		
		while(true){
			
		}
		
	
	}
}

  将上面代码复制3份,每一个服务对应一个main,将代码里面的”服务1”改为对应的服务名字(服务2 服务3以示区别),启动zookeeper集群,然后依次启动3个main方法.可以在控制台下看到同一时刻只有一个服务获取锁,没有获取到锁的时候监听其他节。这里就不复制控制台语句了。
  上面代码有几部说明下
在这里插入图片描述
  上面zookeeper链接的是3个zookeeper地址,也就是我们zookeeper集群的地址,其实一个zookeeper也可以实现这个功能,但是当只有一个的时候zookeeper服务挂掉,那么就无法获取锁。当使用集群的时候,只要有一半的zookeeper能工作,集群就能提供服务。而且还能减少zookeeper 请求创建节点的压力。
在这里插入图片描述
  这部分有一个try catch,这个就是为了下面解决这种特殊情况。当服务1获取锁,处理事务,这时候服务2启动,服务2获取最小节点,就是服务1刚创建的节点,在服务2还没有运行到注册监听的时候,服务1节点已经被删除了,那么服务2的监听事件永远不会触发,导致整个服务出现获取锁卡死情况,都在等待获取锁。所以如果出现上面这种情况,就会捕获NoNodeException异常,服务2主动获取锁,处理事务。
  基本zookeeper提供分布式锁就是这一套流程。下期将介绍zookeeper为分布式环境提供数据同步服务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值