分布式-分布式协调框架Zookeeper(三)——Zookeeper实现分布式锁

注意:当被问到开发中遇到过什么问题时,不要回答报什么错

就回答我们项目是做集群的,做集群的时候涉及到分布式的很多问题,即分布式产生的各种分布式问题,直接回答即可。

这时面试官一般会抽其中几个让你介绍一下,你是怎么解决的,怎么用的,这时候再展开讲。

下面讲一下分布式锁的知识:

一、分布式锁基本概念

1.1、什么是分布式锁

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

1.2、Zookeeper实现分布式锁原理

临时节点+事件通知。

创建 临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完之后此序列节点消失。通过watch来监控节点的变化,从剩下的节点找到最小的序列节点,获取分布式锁,执行相应处理,依次类推.......。

实现步骤:

(1)多个JVM同时在ZK上创建同一个相同的节点/lock;

(2)因为ZK中节点是唯一的,同时如果有多个客户端创建相同的节点/lock,看谁请求创建节点快谁就拿到锁

(3)这时节点类型是临时类型的。如果JVM1已经创建节点成功,那么JVM1和JVM3再创建该节点时会报“/lock节点已经存在”错误,这时JVM2和JVM3进行等待

实现原理详细步骤:

        Zookeeper如何获取锁?

看谁请求创建节点快谁就拿到锁

        Zookeeper如何实现释放锁?

如果jvm1(服务器1)现在已经程序执行完毕,当前ZK已经关闭了Session回话;

这时jvm2(服务器2)和jvm3(服务器3)使用Watcher事件通知获取到/lock已经被删除,这时重新进入到获取锁的请求;这时jvm2和jvm2又回到竞争那一步看谁创建节点快谁就拿到锁,以此类推竞争下去.......

        Zookeeper产生死锁怎么办?

如果程序一直不处理完,可能导致死锁,可以设置有效期,一般60s即可。

二、分布式锁的使用场景

如果是单机版本的系统使用锁可以用lock锁和synchronized锁即可,但是分布式系统的集群环境下就要使用分布式锁。

2.1、分布式场景下生成订单ID

业务场景:分布式情况下,生成全局订单号。

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

分布式情况下如何解决订单号不能重复:

(1)使用分布式锁:

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

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

2.使用redis实现分布式锁(redisson框架实现)

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

3.使用Zookeeper实现分布式锁

实现相对简单、可靠性强,使用临时节点释放锁、失效时间容易控制,效率高又简单

4.SpringCloud内置实现全局锁(用的少)

(2)提前生成好订单号,存放在redis,取订单号直接从redis中取;

 

三、使用Zookeeper实现分布式锁代码(模板方法 设计模式实现)

3.1、Maven依赖

	<dependencies>
		<dependency>
			<groupId>com.101tec</groupId>
			<artifactId>zkclient</artifactId>
			<version>0.10</version>
		</dependency>
	</dependencies>

3.2、创建Lock接口

public interface Lock {
    //获取到锁的资源
	public void getLock();
    // 释放锁
	public void unLock();
}

3.3、创建ZookeeperAbstractLock抽象类

  • 1.连接zkclient,在zk上创建一个/lock节点,节点类型为临时节点
  • 2.如果节点创建成功,直接执行业务逻辑,如果节点创建失败,进行等待
  • 3.使用事件监听该节点是否被删除,如果被删除重新进入获取锁资源

使用的设计模式模板方法模式,就是在父类定义了一个模板,让子类继承父类去实现,面试时做个ZK实现分布式锁还可以吹一下设计模式(即使很简单)!

//将重复代码写入子类中..(设计模式:模板方法模式)
public abstract class ZookeeperAbstractLock implements Lock {
	// zk连接地址
	private static final String CONNECTSTRING = "127.0.0.1:2181";
	// 创建zk连接
	protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
	protected static final String PATH = "/lock";

    //获取锁
	public void getLock() {
        //1.连接zkclient,在zk上创建一个/lock节点,节点类型为临时节点
        //2.如果节点创建成功,直接执行业务逻辑,如果节点创建失败,进行等待
		if (tryLock()) {
			System.out.println("##获取lock锁的资源####");
		} else {
			// 等待
            //3.使用事件监听该节点是否被删除,如果被删除重新进入获取锁资源
			waitLock();
			// 重新获取锁资源
			getLock();
		}

	}

	// 子类实现:获取锁资源,如果成功获取到锁返回true,否则返回false
	abstract boolean tryLock();
	// 子类实现:如果节点创失败,进行等待 ,使用事件监听该节点是否被删除,如果被删除重新进入获取锁资源
	abstract void waitLock();
    //释放锁
	public void unLock() {
        //当程序执行完毕,直接关闭连接
		if (zkClient != null) {
			zkClient.close();
			System.out.println("释放锁资源...");
		}
	}

}

3.4、ZookeeperDistrbuteLock类(子类)

  • 1. 子类实现:获取锁资源,如果成功获取到锁返回true,否则返回false;
  • 2.子类实现:如果节点创失败,进行等待 ,使用事件监听该节点是否被删除,如果被删除重新进入重新进入父类getLock方法——然后进入子类创建节点tryLock方法,重新尝试获取锁。
public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
	private CountDownLatch countDownLatch = null;
//1.获取锁
	@Override
	boolean tryLock() {
		try {
//创建临时节点PATH  获取锁
			zkClient.createEphemeral(PATH);
			return true;//创建成功则返回true
		} catch (Exception e) {
//如果创建该节点失败,这时直接catch
			return false;
		}

	}

//2.如果创建失败,等待
	@Override
	void waitLock() {
		IZkDataListener izkDataListener = new IZkDataListener() {
//表示节点被删除
			public void handleDataDeleted(String path) throws Exception {
				// 唤醒被等待的线程
				if (countDownLatch != null) {
					countDownLatch.countDown();//计数器一旦为0情况
				}
			}
//表示节点被修改
			public void handleDataChange(String path, Object data) throws Exception {

			}
		};
		// 注册监听事件通知
		zkClient.subscribeDataChanges(PATH, izkDataListener);
//如果控制程序等待
		if (zkClient.exists(PATH)) {
			countDownLatch = new CountDownLatch(1);
			try {
//等待
				countDownLatch.await();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

//后面代码继续执行,为了不影响后面代码执行,建议删除该事件监听
		// 删除监听
		zkClient.unsubscribeDataChanges(PATH, izkDataListener);
	}
//为什么要删除事件监听?  因为不删除服务端会多次监听,所以监听完就要删除一下,一走到这一步又重新进入父类getLock方法——然后进入子类创建节点tryLock方法,重新尝试获取锁
}

问题1:为什么要删除事件监听?  

因为不删除服务端会多次监听,所以监听完就要删除一下 

3.5、测试-——使用Zookeeper锁运行效果

public class OrderService implements Runnable {
	private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
	// 使用lock锁
	// private java.util.concurrent.locks.Lock lock = new ReentrantLock();
	private Lock lock = new ZookeeperDistrbuteLock();
	public void run() {
		getNumber();
	}
	public void getNumber() {
		try {
			lock.getLock();
			String number = orderNumGenerator.getNumber();
			System.out.println(Thread.currentThread().getName() + ",生成订单ID:" + number);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unLock();
		}
	}
	public static void main(String[] args) {
		System.out.println("####生成唯一订单号###");
//注意这面这行必须注释掉,因为代表值创建一个ZK的Session连接,只创建一个订单号,所以如果没注释掉只能获取一次锁
//		OrderService orderService = new OrderService();
//		for (int i = 0; i < 100; i++) {
//			new Thread(orderService).start();
//		}

//改成这样,100个循环new100次才能生成100个订单号,获取100次锁
		for (int i = 0; i < 100; i++) {
//这样才能模拟出分布式锁的场景
			new Thread( new OrderService()).start();
		}
	}
}
发布了52 篇原创文章 · 获赞 116 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/RuiKe1400360107/article/details/103796419