一、Zookeeper实现分布式锁概述
1、项目中有使用到分布式锁吗?
案例:需要生成订单号方案
使用UUID、时间戳+业务ID
幂等:就是重复的意思 重复消费等。
高可用:怎么去做就是尽量减少系统宕机的时间。让系统更稳定。
高并发:就是同一时刻,请求同一个接口。
怎么保证接口幂等性 --- 就是怎么保证接口不允许有重复
不要生产重复的。如订单号 保证幂等性。
案例生成订单ID.
怎么实现模拟多用户生成订单号?
答:使用多线程。
二、解决生产订单号线程安全问题(重复生成订单号了)
生成订单号类:OrderNumGenerator.java
package com.leeue.lock;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @classDesc: 功能描述:(生成订单号) 时间戳+业务ID
* @author:李月
* @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:李月
* @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:李月
* @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:李月
* @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:李月
* @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:李月
* @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:李月
* @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:李月
* @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:李月
* @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();
}
}
}
递归抢锁的方式很慢,后期思考下优化方法。