Java架构学习(四十三)Zookeeper实现分布式锁&解决生成订单号的线程安全问题&实现分布式锁解决方案&ZK概述&实现分布式锁基本步骤

一、Zookeeper实现分布式锁概述

1、项目中有使用到分布式锁吗?
案例:需要生成订单号方案
	使用UUID、时间戳+业务ID
	幂等:就是重复的意思  重复消费等。
高可用:怎么去做就是尽量减少系统宕机的时间。让系统更稳定。
高并发:就是同一时刻,请求同一个接口。

怎么保证接口幂等性  --- 就是怎么保证接口不允许有重复  
  不要生产重复的。如订单号 保证幂等性。

案例生成订单ID.
怎么实现模拟多用户生成订单号?
答:使用多线程。  

二、解决生产订单号线程安全问题(重复生成订单号了)

生成订单号类:OrderNumGenerator.java
package com.leeue.lock;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @classDesc: 功能描述:(生成订单号)  时间戳+业务ID
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月15日 下午10:17:58
 */
public class OrderNumGenerator {
	   //全局业务id
		public static int count = 0;

		public String getNumber() {
			SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
			return simpt.format(new Date()) + "-" + ++count;
		}

}
OrderService 订单号业务逻辑(使用多线程模拟用户同时请求)
这里使用了同步代码块 来解决当前生产订单号重复的问题
package com.leeue.lock.service;

import com.leeue.lock.OrderNumGenerator;

/**
 * @classDesc: 功能描述:(订单号生成业务逻辑)
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月15日 下午10:19:58
 */
public class OrderService implements Runnable {

	// 生成订单号
	OrderNumGenerator orderNumGenerator = new OrderNumGenerator();

	public void run() {
		synchronized (this) {
			getNumber();
		}
	
	}

	// 获取流水号
	public void getNumber() {
		String number = orderNumGenerator.getNumber();
		System.out.println(Thread.currentThread().getId() + ",####number:" + number);
	}

	public static void main(String[] args) {

		System.out.println("###模拟生成订单号开始....###");
		OrderService orderService = new OrderService();
		// 模拟多用户同时去请求订单号
		for (int i = 0; i < 100; i++) {
			new Thread(orderService).start();
		}

	}

}

使用lock锁中的重入锁解决 线程安全问题
package com.leeue.lock.service;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.leeue.lock.OrderNumGenerator;

/**
 * @classDesc: 功能描述:(订单号生成业务逻辑)
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月15日 下午10:19:58
 */
public class OrderServicelock implements Runnable {

	// 生成订单号
	OrderNumGenerator orderNumGenerator = new OrderNumGenerator();

	// 这里使用的是lock锁中的重入锁 :可以重复使用的锁
	private Lock lock = new ReentrantLock();

	public void run() {
		try {
			lock.lock();
			getNumber();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {  //释放锁一定要放在这个finally里面,保证执行方法的时候出现异常也能释放掉锁。防止死锁。
			lock.unlock(); //释放锁
		}
	

	}

	// 获取流水号
	public void getNumber() {
		String number = orderNumGenerator.getNumber();
		System.out.println(Thread.currentThread().getId() + ",####number:" + number);
	}

	public static void main(String[] args) {

		System.out.println("###模拟生成订单号开始....###");
		OrderServicelock orderService = new OrderServicelock();
		// 模拟多用户同时去请求订单号
		for (int i = 0; i < 100; i++) {
			new Thread(orderService).start();
		}

	}
}
1、什么是多线程?
答:多线程在应用程序中,有多条不同的执行路径,是并行执行的。
		主要作用就是提高程序效率。
2、在多线程中共享一个全局变量,就可能会受到其他线程干扰,
	这就是会有线程安全问题。主要产生原因是Java内存模型导致的。
	本地内存(副本)中的内容没有及时刷新到(共享内存)主内存中。jmm 可见性()

线程安全问题解决办法?
答:使用sysnchronized 和lock锁来解决。 lock是接口,实现有很多锁,就跟集合	    类似。
多线程同步的时候,一定要使用同一把锁。

synchronized :是自己释放锁。
lock:是手动加锁,手动释放锁。

三、实现分布式锁解决方案

1、怎么保证订单号的幂等性?
答: uuid+时间戳+业务逻辑id
单点系统:使用同步代码块或者使用lock锁机制。
分布式集群环境中:
	在高并发的情况下,时间可能会相同,订单重复问题。

解决分布式情况生产

在这里插入图片描述

解决分布式情况下生成订单号唯一?
1、大公司中,订单号提前生成好,存放在redis环境中。其他服务
器直接从redis中获取订单号信息。
2、使用分布式锁
	什么是分布式锁?
	答:在分布式锁,是多个JVM保证唯一的。 
	1、数据库实现分布式锁
			 数据库释放锁需要自己把数据删除,如果当前tomcat失去了jdbc连接了
			 就释放不了锁了,产生死锁。不推荐使用
	2、使用缓存实现分布式锁 redis实现
			很复杂。与数据库实现是差不多。也是释放不了锁。
	3、使用ZK实现分布式锁。
			推荐使用zookeeper实现分布式锁。

四、Zookeeper概述

什么是Zookeeper?
答:是分布式协调工具。
应用场景:
	1、分布式锁
	2、负载均衡
	3、命名服务 doubber
	4、分布式通知和协调 watcher 事件通知
	5、发布订阅 也可以实现MQ
	6、集群环境 选举策略  类似Redis中的哨兵机制

使用ZK实现分布式锁

使用ZK实现分布式锁原理:zk+临时节点+事件通知+信号量
  zk中节点不能重复的。
1、使用ZK创建一个临时节点(”path“)
2、谁能创建临时节点成功,谁就能拿到锁,谁就能成功创建订单号。
3、什么时候释放锁?临时节点。当tomcat01 断开了与zk的连接,就会删除掉
这个临时节点,这个时候就能去释放锁。
	watcher主要做等待的作用。使用信号量来做。

这里

在这里插入图片描述

	谁能创建这个临时节点成功,谁就能拿到锁,谁就能成功创建订单号。

zk实现分布式锁的原理
答:在zk中创建临时节点,只要服务谁能创建临时节点成功,就能获取锁
其他服务没有创建节点成功,就会一直等待。
其他服务使用事件监听获取节点通知,如果节点以及被删除,应该获取锁的资源。

在这里插入图片描述

五、ZK实现分布式锁

第一、创建关于自动生成流水号的类OrderNumGenerator 
package com.leeue;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 *
 * @classDesc: 功能描述:(第一步 创建好生成订单号的类) 业务ID+时间
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月26日 上午9:47:59
 */
public class OrderNumGenerator {

	// 全局ID
	public static int ID = 0;

	public String getNumber() {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
		ID++;
		return sdf.format(new Date()) + "-" + ID;
	}

}

第二步:将自定义zk锁抽象成接口来做
package com.leeue.lock;

/**
 * 
 * @classDesc: 功能描述:(第二步 将自定义的zk锁 抽象出成接口)
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月26日 上午9:53:19
 */
public interface IZKLock {

	/***
	 * 获取锁资源
	 */
	public void getLock();

	/**
	 * 释放锁资源
	 */
	public void unLock();
}

	第三步:重构一些代码,将重复的代码放在抽象类中
package com.leeue.lock;
/**
 * 
 * @classDesc: 功能描述:(第三步 重构重复代码 将一些重复的代码放在抽象类中 )
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月26日 上午9:57:43
 */

import java.util.concurrent.CountDownLatch;

import org.I0Itec.zkclient.ZkClient;

public abstract class AbstractZookeeperLock implements IZKLock{
	// 1.zk连接地址ip+端口号
	private static final String CONNECT_ADDRESS = "127.0.0.1:2181";
	//2.创建zk连接
	protected ZkClient zkClient = new ZkClient(CONNECT_ADDRESS);
	//3.创建临时节点
	protected  static final String PATH = "/lock";

	//4.信号量定义 countdownLatch 置为 null 当前等于 0的时候,唤醒在其间等待的线程
	protected CountDownLatch countDownLatch = null;
	
	/**
	 * 尝试获取锁  如果没有获取到锁,就继续等待获取锁 waitGetLock(),获取到了锁就开始执行生产 订单。
	 * 获取到锁 就是创建这个PATH节点成功了 则当前线程就获取到锁了
	 * @return
	 */
	abstract Boolean tryGetLock();
	
	/**
	 * 等待获取锁
	 */
	abstract void waitGetLock();
	/**
	 * 实现重复方法  获取锁  递归
	 */
	public void getLock() {
		if(tryGetLock()) {
			System.out.println("####获取锁成功#####");
		}else {
			//等待获取锁  使用的信号量和ZK的事件监听来实现,等待完毕后再尝试获取锁
			waitGetLock();
			//再尝试获取锁
			getLock();
		}
	}
	
	/**
	 * 释放锁资源  释放锁资源就是 关闭zk连接就行了
	 */
	public void unLock() {
		
		if(zkClient!=null) {
			System.out.println("####释放锁资源####");
			System.out.println();
			zkClient.close();
			
		}
	}


}

第四步:实现抽象类的具体方法  实现基本步骤
package com.leeue.lock;

import java.util.concurrent.CountDownLatch;

import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;

/***
 * 
 * @classDesc: 功能描述:(第四步  创建实现ZK锁的子类  继承上一个的抽象类  获取锁 和等待锁两个方法)
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月26日 上午10:18:22
 */
public class ZookeeperDistrbuteLock extends AbstractZookeeperLock{

	@Override
	Boolean tryGetLock() {
		if(zkClient!=null) {
			//创建节点 如果当前节点创建成功了 就不会报异常  就返回true
			//如果创建失败了就返回 true
			try {
				zkClient.createEphemeral(PATH);
				return true;
			} catch (Exception e) {
				//走到异常里面去了 说明没有创建成功
				return false;
			}
		
		}
		return null;
	}

	@Override
	void waitGetLock() {
		
		
		
		//1.使用事件监听 查看当前节点是否被删除了  IZkDataListener是对节点事件(数据事件)的监听
		IZkDataListener iZkDataListener = new IZkDataListener() {
			
			//2.只监听删除事件就可以了  当节点被删除 信号量不为空 的时候 该节点信号量就--,唤醒拥有这个信号量的等待线程,
			//让他去尝试获取锁
			public void handleDataDeleted(String arg0) throws Exception {
				if(countDownLatch!=null) {
					//唤醒等待的线程
					countDownLatch.countDown();
				}
			}
			
			public void handleDataChange(String arg0, Object arg1) throws Exception {
				// TODO 不需要监听的
			}
		};
		
		//3. 将上面的监听信息注册到客户端  subscribeDataChanges  是对节点信息进行注册的
		zkClient.subscribeDataChanges(PATH,iZkDataListener);
		//4.再检测当前节点是否存在
		if(zkClient.exists(PATH)) {
			//如果当前节点已经存在了  信号量就会控制当前线程等待
			CountDownLatch countDownLatch = new CountDownLatch(1);
			try {
				//让当前信号量等待
				countDownLatch.await();
			} catch (Exception e) {
				// TODO: handle exception
			}
		}
		//如果当前节点不存在 说明这个节点现在已经在这个线程上了获取到了,就删除这个节点的监听了
		zkClient.unsubscribeDataChanges(PATH, iZkDataListener);
		
	}

}

第五步:生成流水号的线程
package com.leeue.lock.service;
/**
 * 
 * @classDesc: 功能描述:(第五步生成订单号的线程  )
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月26日 上午11:13:49
 */

import com.leeue.OrderNumGenerator;
import com.leeue.lock.IZKLock;
import com.leeue.lock.ZookeeperDistrbuteLock;

public class OrderServicesynchronized implements Runnable{
	//1.生成订单号
	OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
	
	private IZKLock zkLock = new ZookeeperDistrbuteLock();
	
	public void run() {
		try {
			//获取锁资源
			zkLock.getLock();
			getNumber();
			
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			//释放锁资源
			zkLock.unLock();
		}
	
	}
	// 获取流水号
	public void getNumber() {
		String number = orderNumGenerator.getNumber();
		System.out.println(Thread.currentThread().getId() + ",####number:" + number);
	}
	
}

启动类:
package com.leeue;

import com.leeue.lock.service.OrderServicesynchronized;

/**
 * @classDesc: 功能描述:(启动ZK分布式锁)
 * @author:<a href="leeue@foxmail.com">李月</a>
 * @Version:v1.0
 * @createTime:2018年11月26日 下午1:46:05
 */
public class ZKLockApp {
	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
			new Thread(new OrderServicesynchronized()).start();
		}

	}
}


递归抢锁的方式很慢,后期思考下优化方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值