(二)分布式锁解决方案-Zookeeper两种实现方式(zkclient实现)

业务场景

在分布式情况,生成全局订单号ID

产生问题

在分布式(集群)环境下,每台JVM不能实现同步,在分布式场景下使用时间戳生成订单号可能会重复

 

分布式情况下,怎么解决订单号生成不重复

  1. 使用分布式锁
  2. 提前生成好,订单号,存放在redis取。获取订单号,直接从redis中取。

使用分布式锁生成订单号技术

1.使用数据库实现分布式锁

缺点:性能差、线程出现异常时,容易出现死锁

2.使用redis实现分布式锁

缺点:锁的失效时间难控制、容易产生死锁、非阻塞式、不可重入 

3.使用zookeeper实现分布式

实现相对简单、可靠性强、使用临时节点失效时间容易控制

什么是分布式

分布式锁一般用在分布式系统或者多个应用中,用来控制同一任务是否执行或者任务的执行顺序。在项目中,部署了多个tomcat应用,在执行定时任务时就会遇到同一任务可能执行多次的情况,我们可以借助分布式锁,保证在同一时间只有一个tomcat应用执行了定时任务

 

使用Zookeeper实现分布式锁

Zookeeper实现分布式锁原理

使用zookeeper创建临时序列节点来实现分布式锁,适用于顺序执行的程序,大体思路就是创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理,依次类推……

 

封装工具类zkClient的实现

@Configuration
public class ZkSessionConfig {

	private static final Logger logger = LoggerFactory.getLogger(ZkSessionConfig.class);

	@Value("${zookeeper.address:localhost:2181}")
	private  static String COONECTSTRING="localhost:2181";

	@Value("${zookeeper.timeout:5000}")
	private  static int sessionTimeout=5000;

	private final static CountDownLatch countDownLatch = new CountDownLatch(1);

	/**
	 * 配置读取的属性值跟havingValue做比较,如果一样则返回true;否则返回false
	 * 如果返回值为false,则该configuration不生效;为true则生效 matchIfMissing =
	 * true表示如果没有在application.properties设置该属性,则默认为条件符合
	 * 
	 * @return
	 */
	@Bean(name = "zkClient")
	@ConditionalOnProperty(name = "zk.enabled", havingValue = "true", matchIfMissing = false)
	public ZooKeeper zkClient() {
		ZooKeeper zooKeeper = null;
		try {
			zooKeeper = new ZooKeeper(COONECTSTRING, sessionTimeout, new Watcher() {
				@Override
				public void process(WatchedEvent event) {
					// 获取事件类型
					EventType eventType = event.getType();
					// zk 路径
					String path = event.getPath();
					// 如果当前的连接状态是成功的,则通过计算器去控制
					if (KeeperState.SyncConnected == event.getState()) {
						// 如果收到了服务端的响应事件,连接成功
						if (EventType.None == eventType) {
							// 如果建立建立成功,让后程序往下走
							logger.info("【事件通知】,zk 建立连接成功!");
							countDownLatch.countDown();
						}else if (EventType.NodeCreated == eventType) {
							logger.info("【事件通知】,新增node节点" + path);
						} else if (EventType.NodeDataChanged == eventType) {
							logger.info("【事件通知】,当前node节点" + path + "被修改....");
						} else if (EventType.NodeDeleted == eventType) {
							logger.info("【事件通知】,当前node节点" + path + "被删除....");
						}
					}
					
				}
			});
			countDownLatch.await();
			logger.info("【初始化ZooKeeper连接状态....】={}", zooKeeper.getState());
		} catch (IOException | InterruptedException e) {
			logger.error("【初始化ZooKeeper连接异常....】={}", e);
			e.printStackTrace();
		}
		return zooKeeper;
	}

	public static void main(String[] args) {
		try {
			ZooKeeper zookeeper = new ZooKeeper(COONECTSTRING, sessionTimeout, new Watcher() {
				@Override
				public void process(WatchedEvent event) {
					// 获取事件类型
					EventType eventType = event.getType();
					// zk 路径
					String path = event.getPath();
					// 如果当前的连接状态是成功的,则通过计算器去控制
					if (KeeperState.SyncConnected == event.getState()) {
						// 如果收到了服务端的响应事件,连接成功
						if (EventType.None == eventType) {
							// 如果建立建立成功,让后程序往下走
							logger.info("【事件通知】,zk 建立连接成功!");
							countDownLatch.countDown();
						}else if (EventType.NodeCreated == eventType) {
							logger.info("【事件通知】,新增node节点" + path);
						} else if (EventType.NodeDataChanged == eventType) {
							logger.info("【事件通知】,当前node节点" + path + "被修改....");
						} else if (EventType.NodeDeleted == eventType) {
							logger.info("【事件通知】,当前node节点" + path + "被删除....");
						}
					} 
					
				}
			});
			String path="/edu_temp1";
			countDownLatch.await();
			System.out.println(zookeeper.getState());
			zookeeper.exists(path, true);
//			1.创建持久节点,并且允许任何服务器可以操作
//			String result1 = zookeeper.create("/edu_Lasting", "Lasting".getBytes(), Ids.OPEN_ACL_UNSAFE,
//					CreateMode.PERSISTENT);
//			System.out.println("result:" + result1);
//			2.创建临时节点
			String result2 = zookeeper.create(path, "temp".getBytes(), Ids.OPEN_ACL_UNSAFE,
					CreateMode.PERSISTENT);
			System.out.println("result:" + result2);
			Thread.sleep(10000L);
		} catch (IOException | InterruptedException | KeeperException e) {
			e.printStackTrace();
		}
	}

}
@Component
@ConditionalOnProperty(name="zk.enabled", havingValue="true", matchIfMissing=false)
public class ZkSessionUtil {
	
	private static final Logger logger = LoggerFactory.getLogger(ZkSessionUtil.class);
	
	@Autowired
	private ZooKeeper zkClient;
	
	
	 /**
     * 判断指定节点是否存在
     * @param path
     * @param needWatch  指定是否复用zookeeper中默认的Watcher
     * @return
     */
    public Stat exists(String path, boolean needWatch){
        try {
            return zkClient.exists(path,needWatch);
        } catch (Exception e) {
            logger.error("【断指定节点是否存在异常】{},{}",path,e);
            return null;
        }
    }

    /**
     *  检测结点是否存在 并设置监听事件
     *      三种监听类型: 创建,删除,更新
     *
     * @param path
     * @param watcher  传入指定的监听类
     * @return
     */
    public Stat exists(String path,Watcher watcher ){
        try {
            return zkClient.exists(path,watcher);
        } catch (Exception e) {
            logger.error("【断指定节点是否存在异常】{},{}",path,e);
            return null;
        }
    }

    /**
     * 创建持久化节点
     * @param path
     * @param data
     */
    public boolean createNode(String path, String data){
        try {
            zkClient.create(path,data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
            return true;
        } catch (Exception e) {
            logger.error("【创建持久化节点异常】{},{},{}",path,data,e);
            return false;
        }
    }


    /**
     * 修改持久化节点
     * @param path
     * @param data
     */
    public boolean updateNode(String path, String data){
        try {
            //zk的数据版本是从0开始计数的。如果客户端传入的是-1,则表示zk服务器需要基于最新的数据进行更新。如果对zk的数据节点的更新操作没有原子性要求则可以使用-1.
            //version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查
            zkClient.setData(path,data.getBytes(),-1);
            return true;
        } catch (Exception e) {
            logger.error("【修改持久化节点异常】{},{},{}",path,data,e);
            return false;
        }
    }

    /**
     * 删除持久化节点
     * @param path
     */
    public boolean deleteNode(String path){
        try {
            //version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查
            zkClient.delete(path,-1);
            return true;
        } catch (Exception e) {
            logger.error("【删除持久化节点异常】{},{}",path,e);
            return false;
        }
    }

    /**
      * 获取当前节点的子节点(不包含孙子节点)
      * @param path 父节点path
      */
    public List<String> getChildren(String path) throws KeeperException, InterruptedException{
        List<String> list = zkClient.getChildren(path, false);
        return list;
    }

    /**
     * 获取指定节点的值
     * @param path
     * @return
     */
    public  String getData(String path,Watcher watcher){
        try {
            Stat stat=new Stat();
            byte[] bytes=zkClient.getData(path,watcher,stat);
            return  new String(bytes);
        }catch (Exception e){
            e.printStackTrace();
            return  null;
        }
    }
    
    
	public static void main(String[] args) {
		
	}
	

}

创建Lock接口

public interface ZkClientLock {
	// 成功 ---阻塞---失败
	public void lock();

	// 成功 --失败
	public void unlock();
}

创建ZookeeperAbstractLock抽象

public abstract class ZkClientAbstractLock implements ZkClientLock {

	private static Logger logger = LoggerFactory.getLogger(ZkClientAbstractLock.class);
//	// 定义session超时时间===>网络不稳定==》闪断
	// 如果没有定义好SESSION_TIMEOUT,有可能在一个锁中会有两个进程在操作
//	private static final int SESSION_TIMEOUT = 10000;

	// 默认连接时间
	// zk服务决定问题是否小号你的锁id对应的节点问题
//	private static final int CONNETCTION_TIMEOUT = 1000;
//
	protected static final String path = "/jetsen-zk-lock";
//
//	// ZK服务字符串
//	private String zkServers = "localhost:2181";

//	public ZkClient zkClient = new ZkClient(zkServers, SESSION_TIMEOUT, CONNETCTION_TIMEOUT);

	public ZkClient zkClient = new ZkClientConfig().getZkClient();
	
	/**
	 * lock 是一个有阻塞中间状态的方法 1、高并发多线程上锁---》获取锁的行为====》如果失败,阻塞等待(中间的状态 )
	 * 2、获取锁成功====》进行业务方法的调用 3、等待的状态何时结束,结束的时候有没有通知唤醒===》再次尝试争抢锁
	 */
	@Override
	public void lock() {
		if (trylock()) {
			// 成功===》执行业务代码
			logger.info(Thread.currentThread().getName() + "get distrubtedLock success");
		} else {
			// 失败===》阻塞等待状态
			waitForLock();
		}
	}

	/**
	 * 
	 */
	@Override
	public void unlock() {
//		// Spring 有效性检查
		if (zkClient.exists(path)) {
			zkClient.delete(path);
			if (zkClient != null) {
				zkClient.close();
			}
		}
	}

	// 扩展定义2中的中间状态
	// 1、失败后等待(中间的状态)
	protected abstract boolean waitForLock();

	// 2 再次尝试争抢锁(中间的状态)
	protected abstract boolean trylock();
}
public  class ZkClientDistrbuteLockImpl extends ZkClientAbstractLock{

	private static Logger logger = LoggerFactory.getLogger(ZkClientDistrbuteLockImpl.class);
	
	private static CountDownLatch latch;
	
	/**
	 * waitForLock 当期path被占用
	 * 设计等待方法
	 * 1、对当前分布式节点注册监听器,对删除事件感兴趣
	 * 如果节点被删除  ===》唤醒争抢====》waitForLock状态结束
	 */
	@Override
	protected boolean waitForLock() {
		//当前线程争抢锁失败的线程
		//1、new一个监听器(ZK分布式架构使用)-===》放在分布式节点上面
		IZkDataListener listener = new IZkDataListener() {
			
			@Override
			public void handleDataChange(String dataPath, Object data) throws Exception {
				logger.info("当前节点"+dataPath+"数据发生变化!");
			}
			
			
			//3、最关注的就是PATH节点的删除====》释放锁
			@Override
			public void handleDataDeleted(String dataPath) throws Exception {
				//唤醒 限流器里面的线程,让一起去争抢
				if(latch!=null){
					latch.countDown();
					//线程计数器-1 马上争抢 
					lock();
				}
			}
		};
		
		//2 创建出来的监听器listener注册到path
		zkClient.subscribeDataChanges(path, listener);
		
		//3 当前节点是否还存在
		if(zkClient.exists(path)){
			latch = new CountDownLatch(1);
		}
		zkClient.unsubscribeDataChanges(path, listener);
		return false;
	}
	
	/**
	 * 通过在zk尝试获取分布式锁 一旦发现执行异常(ZkNodeException)说明你获取了锁失败
	 * 说明其中有一个线程获取锁成功,创建一个PATH节点
	 */
	@Override
	protected boolean trylock() {
		try {
			zkClient.createEphemeral(path);
			logger.info("创建分布式锁成功!");
			return true;
		} catch (Exception e) {
			logger.info("创建分布式锁失败,已被其他线程占用");
			return false;
		}
	}
	
}
public class ZkClientLockUtil {
	
	public ZkClientLockUtil() {
		
	}

	public static String idGenerator( IGenerateGlobalId iGenerateGlobalId) {
		ZkClientLock lock = new ZkClientDistrbuteLockImpl();
		String idGenerator = null;
		try {
			lock.lock();
			idGenerator = iGenerateGlobalId.idGenerator();
			System.out.println(Thread.currentThread().getName() + ",生成ID:" + idGenerator);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
		return idGenerator;
	}
	
	public static void main(String[] args) {
		
		for (int i = 0; i < 10; i++) {
			idGenerator(new SequenceUnLockGenerateGlobalIdImpl());
		}
	}
}
public class ZkClientTestTask implements Runnable {

	private IGenerateGlobalId iGenerateGlobalId;

	private CountDownLatch latch;
	

	public ZkClientTestTask(IGenerateGlobalId iGenerateGlobalId, CountDownLatch latch) {
		this.iGenerateGlobalId = iGenerateGlobalId;
		this.latch = latch;
	}

	@Override
	public void run() {
		try {
			String idGenerator = ZkClientLockUtil.idGenerator(iGenerateGlobalId);
			System.out.printf("线程名称:%s 订单号:%s \r\n", Thread.currentThread().getName(),idGenerator);
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
		}
	}
	
	public static void main(String[] args) {
		IGenerateGlobalId entity = new SequenceUnLockGenerateGlobalIdImpl();
		ExecutorService pool = Executors.newCachedThreadPool();
		final CountDownLatch latch = new CountDownLatch(1);
		for(int i=0;i<10;i++) {
			pool.execute(new ZkClientTestTask(entity,latch));
		}
		latch.countDown();
		pool.shutdown();
	}

}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值