文章目录
- 一、前言
- 二、从锁到分布式锁
- 2.1 分布式技术四个分支
- 2.2 从锁到分布式锁(为什么redis需要分布式锁)
- 2.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建
- 2.4 zookeeper四种节点
- 三、进程内无锁--进程内加锁--zookeeper分布式锁--zookeeper分布式锁
- 3.1 进程内不是锁,造成线程安全问题
- 3.2 进程内使用锁(synchronized / lock)
- 3.3 zookeeper分布式锁(加锁+解锁+监听)
- 3.4 zookeeper分布式锁(加锁+解锁+监听+解决羊群效应)
- 四、面试金手指
- 4.1 分布式 + 从锁到分布式锁 + zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建
- 4.1.1 分布式技术四个分支
- 4.1.2 从锁到分布式锁(为什么redis需要分布式锁)
- 4.1.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建
- 4.2 zookeeper是如何基于节点去实现各种分布式锁的?
- 五、小结
一、前言
二、从锁到分布式锁
2.1 分布式技术四个分支
分布式相关技术:分布式部署导致分布式锁、分布式事务、分布式登录(单点登录)、分布式存储HDFS四个技术的出现
1、后端服务分布式部署后,进程内Java并发多线程锁(阻塞锁+CAS)不适用了,出现了分布式锁(三种实现方式:直接在Mysql表上实现分布式锁、引入一个中间件zookeeper实现各服务之间分布式锁、引入一个中间件redis实现各服务之间分布式锁)
2、mysql分布式部署后,事务不适用了,出现了分布式事务(七种实现方式:两阶段、三阶段、XA、最大努力、ebay本地消息表、TCC编程模式、半消息/最终一致性)
3、后端服务分布式部署后,进程内登录模块,服务端session不适用了,出现了分布式登录,即单点登录
4、分布式存储HDFS
2.2 从锁到分布式锁(为什么redis需要分布式锁)
单个Java进程内的锁:
问题:在Java并发多线程环境下,由于上下文的切换,数据可能出现不一致的情况或者数据被污染。
期待:当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。
实现两种方式:Java中实现锁有两种方式,阻塞锁(synchronized lock)或 CAS硬件层面锁。
进程内的多线程锁
互斥:互斥的机制,保证同一时间只有一个线程可以操作共享资源 synchronized,Lock等。
临界值:让多线程串行话去访问资源
事件通知:通过事件的通知去保证大家都有序访问共享资源
信号量:多个任务同时访问,同时限制数量,比如发令枪CDL(即CountDownLatch),Semaphore等
问题:为什么Redis是单线程,但是需要分布式锁?
标准答案:Redis是单线程,所以单个redis内部不需要进程内锁机制,这是可以确定的;
如果后端采用单体架构,一个Redis对应一个服务,不需要在redis中实现分布式锁;
如果后端采用微服务架构,一个Redis对应多个SOA服务,需要在redis中实现分布式锁。
解释:如果后端采用微服务架构,不使用任何分布式锁机制(mysql、zookeeper、redis)
电商网站中的秒杀,拿到库存判断,然后在减库存,双11之前,为了减少DB的压力,挡住第一批最大秒杀,把库存预热到了KV,现在KV的库存是1。服务A去Redis查询到库存发现是1,那说明我能抢到这个商品对不对,那我就准备减一了,但是还没减。同时服务B也去拿发现也是1,那我也抢到了呀,那我也减。C同理。等所有的服务都判断完了,你发现诶,怎么变成-2了,超卖了呀,这下完了。
2.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建
zookeeper开发中的使用场景/zookeeper有什么用:
开发用途1:统一配置管理,服务注册与订阅(共用节点,类似于eureka,作为注册中心)
开发用途1:统一配置管理,数据订阅与发布(使用 watcher 实现)
开发用途2:统一命名管理,服务命名(使用 znode特性 实现)
开发用途3:分布式锁(使用 临时顺序节点 实现,三种分布式锁:mysql zookeeper )
开发用途4:集群管理,分布式通知(使用 监听znode 实现)
金手指:zookeeper存储
问题1:zookeeper是什么?
回答1:zookeeper是个数据库,文件存储系统,并且有监听通知机制(观察者模式)zookeeper作为一个类Unix文件系统,zookeeper通过节点来存储数据。
问题2:接上面,zookeeper节点?
回答2:zk的节点类型有4大类:持久化节点(zk断开节点还在)、持久化顺序编号目录节点、临时目录节点(客户端断开后节点就删除了)、临时目录编号目录节点。
注意:节点名称都是唯一的,这是zookeeper实现分布式锁互斥的基石。
2.4 zookeeper四种节点
使用层面:zookeeper节点怎么创建(linux操作)?
在Zookeeper的数据模型中,Znode分为四种:持久(persistent), 临时(ephemeral),持久序列(persistent_sequential), and 临时序列(ephemeral_sequential)。
从zkCli创建zookeeper znode的创建语法如下
create [-s] [-e] path data // path 表示新节点的路径 data表示节点名 -s 表示有序 -e 表示临时
create /test laogong // 创建永久节点
create -e /test laogong // 创建临时节点
create -s /test // 创建顺序节点
create -e -s /test // 创建临时顺序节点
临时节点与永久节点:退出后,重新连接,上一次创建的所有临时节点都没了,但是永久节点还有。
1、持久节点
持久模式的znode,即使创建该znode的client断开了连接,znode依然存在。当重启zookeeper之后,持久模式的znode也会重新被加载到内存中;
持久节点的znode可以有child node;
2、临时节点
临时模式的znode,其生命周期与创建其的client相同,当创建其的client与zookeeper服务断开连接时,该临时节点就自动删除。
临时节点znode不能有child node
3、持久有序节点
使用持久序列模式创建一个znode时,znode会自动增加一个编号;
持久模式的znode可以有child node;
4、临时有序节点
在临时模式的基础上,增加了一个自动编号的功能。
临时znode不能有child node,一般当做其他Node的子Node。
小结1(使用层面:创建节点):create [-s] [-e] path data // path 表示新节点的路径 data表示节点名 -s 表示有序 -e 表示临时
小结2(使用层面:子节点):持久节点的znode可以有child node,临时节点的znode可以有child node
三、进程内无锁–进程内加锁–zookeeper分布式锁–zookeeper分布式锁
3.1 进程内不是锁,造成线程安全问题
public class Test implements Runnable {
static int inventory = 1; // 要在main函数中用,设置为static
private static final int NUM = 10; // 要在main函数中用,设置为static
private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static
public static void main(String[] args) {
Test test = new Test(); // 10个线程都是用同一个test对象
for (int i = 1; i <= NUM; i++) {
new Thread(test).start(); // 新建线程并启动
countDownLatch.countDown(); // 减1
}
}
@Override
public void run() { // 线程运行的具体逻辑在run()里面
try {
countDownLatch.await(); // 一定到countDownLatch==0才可以通过阻塞 第一,为什么要使用发令枪?不用发令枪也可以,使用发令枪只是让10个线程启动更加同时,不会先创建的线程先启动
if (inventory > 0) { // 就是inventory ==1 可以进入
Thread.sleep(1); // 第二,为什么加上Thread.sleep() 在 if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显
inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显
}
System.out.println(inventory);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
-1
-1
-2
-3
-3
-3
-3
-3
-3
-3
解释代码:
第一,为什么要使用发令枪?不用发令枪也可以,使用发令枪只是让10个线程启动更加同时,不会先创建的线程先启动
第二,为什么加上Thread.sleep()? 在 if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显
3.2 进程内使用锁(synchronized / lock)
public class Test1 implements Runnable {
private final Lock _lock = new ReentrantLock();
static int inventory = 1; // 要在main函数中用,设置为static
private static final int NUM = 10; // 要在main函数中用,设置为static
private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static
public static void main(String[] args) {
Test1 test1=new Test1();
for (int i = 1; i <= NUM; i++) {
new Thread(test1).start(); // 新建线程并启动 第三,这个10个线程,一定要传入同一个对象test1,否则锁不起作用
countDownLatch.countDown(); // 减1
}
}
@Override
public void run() { // 线程运行的具体逻辑在run()里面
_lock.lock();
try {
countDownLatch.await(); // 一定到countDownLatch==0才可以通过阻塞
if (inventory > 0) { // 就是inventory ==1 可以进入
Thread.sleep(5); // 休息5ms 让线程安全问题暴露的更明显
inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显
}
System.out.println(inventory);
} catch (Exception e) {
e.printStackTrace();
} finally {
_lock.unlock();
}
// try {
// countDownLatch.await(); // 一定到countDownLatch==0才可以通过阻塞
// synchronized (this){
// if (inventory > 0) { // 就是inventory ==1 可以进入
// Thread.sleep(5); // 休息5ms 让线程安全问题暴露的更明显
// inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显
// }
// System.out.println(inventory);
// }
//
// } catch (Exception e) {
// e.printStackTrace();
// }
}
}
解释代码:
第一,为什么要使用发令枪?不用发令枪也可以,使用发令枪只是让10个线程启动更加同时,不会先创建的线程先启动
第二,为什么加上Thread.sleep()? 在 if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显
第三,这个10个线程,一定要传入同一个对象test1,否则锁不起作用
3.3 zookeeper分布式锁(加锁+解锁+监听)
public class Test2 implements Runnable {
private static final String IP_PORT = "127.0.0.1:2181";
private static final String Z_NODE = "/LOCK";
private static final int NUM = 10; // 要在main函数中用,设置为static
private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static
private static ZkClient zkClient = new ZkClient(IP_PORT);
static int inventory = 1; // 要在main函数中用,设置为static
private static final int NUM = 10; // 要在main函数中用,设置为static
public static void main(String[] args) {
Test2 test2 = new Test2(); // 10个线程都是用同一个test对象
for (int i = 1; i <= NUM; i++) {
new Thread(test2).start(); // 新建线程并启动 分布式锁3:原来是一个进程中10个线程,用发令枪控制10个线程同时跑
// 现在,一个后端节点一个线程,所以10个后端节点10个线程,不需要发令枪了,它只能控制同一进程里面的线程一起跑
}
}
@Override
public void run() { // 线程运行的具体逻辑在run()里面
try {
new Test2().lock();
zkClient.createPersisent(Z_NODE); // 分布式锁1:新建一个节点 节点是唯一的,已经有了,再次新建就会报错,用新建节点这个特性来实现分布式锁,相当于synchronized | lock
if (inventory > 0) { // 就是inventory ==1 可以进入
Thread.sleep(1); // 第二,为什么加上Thread.sleep() 在 if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显
inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显
}
System.out.println(inventory);
return;
} finally { // 分布式锁4:去掉catch块
new Test2().unlock();
// 分布式锁2:在finally块,释放节点,类似lock.unlock 即,最开始新建节点类似lock.lock() finally块释放节点类似 lock.unlock();
System.out.println("释放锁");
}
}
public void lock() {
if (tryLock()) { // 加锁成功,直接return
return;
}
waitForLock(); // 加锁失败,然后等待,突破发令枪阻塞再开始
lock(); // 突破发令枪阻塞,再次尝试加锁
}
public void waitForLock() { // 监听
System.out.println("加锁失败");
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String arg0, Object arg1) throws Exception { // 修改
}
@Override
public void handleDataDeleted(String arg0) throws Exception {
System.out.println("监听到配置文件被删除,唤醒"); // config删除,监听打印
}
};
zkClient.subscribeDataChanges(Z_NODE, listener);
if (zkClient.exists(Z_NODE)) {
try {
countDownLatch.await(); // 发令枪阻塞,从这里开始
} catch (Exception e) {
e.printStackTrace();
}
}
zkClient.unsubscribeDataChanges(Z_NODE, listener);
}
}
3.4 zookeeper分布式锁(加锁+解锁+监听+解决羊群效应)
public class Test3 implements Runnable {
private static final String IP_PORT = "127.0.0.1:2181";
private static final String Z_NODE = "/LOCK";
private static final int NUM = 10; // 要在main函数中用,设置为static
private static CountDownLatch countDownLatch = new CountDownLatch(NUM); // 要在main函数中用,设置为static
private static ZkClient zkClient = new ZkClient(IP_PORT);
static int inventory = 1; // 要在main函数中用,设置为static
private static final int NUM = 10; // 要在main函数中用,设置为static
public static void main(String[] args) {
Test3 test3 = new Test3(); // 10个线程都是用同一个test对象
for (int i = 1; i <= NUM; i++) {
new Thread(test3).start(); // 新建线程并启动 分布式锁3:原来是一个进程中10个线程,用发令枪控制10个线程同时跑
// 现在,一个后端节点一个线程,所以10个后端节点10个线程,不需要发令枪了,它只能控制同一进程里面的线程一起跑
}
}
@Override
public void run() { // 线程运行的具体逻辑在run()里面
try {
new Test2().lock();
zkClient.createPersisent(Z_NODE); // 分布式锁1:新建一个节点 节点是唯一的,已经有了,再次新建就会报错,用新建节点这个特性来实现分布式锁,相当于synchronized | lock
if (inventory > 0) { // 就是inventory ==1 可以进入
Thread.sleep(1); // 第二,为什么加上Thread.sleep() 在 if (inventory > 0)和 inventory --;中间加上Thread.sleep(); 让线程安全问题暴露的更明显
inventory--; // 在inventory--,前面休息5ms 让线程安全问题暴露的更明显
}
System.out.println(inventory);
return;
} finally { // 分布式锁4:去掉catch块
new Test2().unlock();
// 分布式锁2:在finally块,释放节点,类似lock.unlock 即,最开始新建节点类似lock.lock() finally块释放节点类似 lock.unlock();
System.out.println("释放锁");
}
}
public void lock() {
if (tryLock()) { // 加锁成功,直接return
System.out.println("获得锁");
} else {
waitForLock(); // 加锁失败,然后等待,突破发令枪阻塞再开始
lock(); // 突破发令枪阻塞,再次尝试加锁
}
}
public synchronized boolean tryLock() {
if (StringUtils.isBlank(path))
path = zkClient.createEphemeralSequential(Z_NODE + "/", "lock");
List<String> children = zkClient.getClildren(Z_NODE);
Collections.sort(children);
if (path.equals(Z_NODE + "/" + children.get(0))){
System.out.println("I am true");
return true;
}else {
int i=Collections.binarySearch(children,path.substring(Z_NODE.length() + 1));
beforePath= Z_NODE +"/"+children.get(i-1);
}
return false;
}
public void waitForLock() {
System.out.println("加锁失败");
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String arg0, Object arg1) throws Exception { // 修改
}
@Override
public void handleDataDeleted(String arg0) throws Exception {
System.out.println("监听到配置文件被删除,唤醒"); // config删除,监听打印
}
};
zkClient.subscribeDataChanges(beforePath, listener);
if (zkClient.exists(Z_NODE)) {
try {
countDownLatch.await(); // 发令枪阻塞,从这里开始
} catch (Exception e) {
e.printStackTrace();
}
}
zkClient.unsubscribeDataChanges(beforePath, listener);
}
}
四、面试金手指
4.1 分布式 + 从锁到分布式锁 + zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建
4.1.1 分布式技术四个分支
分布式相关技术:分布式部署导致分布式锁、分布式事务、分布式登录(单点登录)、分布式存储HDFS四个技术的出现
1、后端服务分布式部署后,进程内Java并发多线程锁(阻塞锁+CAS)不适用了,出现了分布式锁(三种实现方式:直接在Mysql表上实现分布式锁、引入一个中间件zookeeper实现各服务之间分布式锁、引入一个中间件redis实现各服务之间分布式锁)
2、mysql分布式部署后,事务不适用了,出现了分布式事务(七种实现方式:两阶段、三阶段、XA、最大努力、ebay本地消息表、TCC编程模式、半消息/最终一致性)
3、后端服务分布式部署后,进程内登录模块,服务端session不适用了,出现了分布式登录,即单点登录
4、分布式存储HDFS
4.1.2 从锁到分布式锁(为什么redis需要分布式锁)
单个Java进程内的锁:
问题:在Java并发多线程环境下,由于上下文的切换,数据可能出现不一致的情况或者数据被污染。
期待:当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。
实现两种方式:Java中实现锁有两种方式,阻塞锁(synchronized lock)或 CAS硬件层面锁。
进程内的多线程锁
互斥:互斥的机制,保证同一时间只有一个线程可以操作共享资源 synchronized,Lock等。
临界值:让多线程串行话去访问资源
事件通知:通过事件的通知去保证大家都有序访问共享资源
信号量:多个任务同时访问,同时限制数量,比如发令枪CDL(即CountDownLatch),Semaphore等
问题:为什么Redis是单线程,但是需要分布式锁?
标准答案:Redis是单线程,所以单个redis内部不需要进程内锁机制,这是可以确定的;
如果后端采用单体架构,一个Redis对应一个服务,不需要在redis中实现分布式锁;
如果后端采用微服务架构,一个Redis对应多个SOA服务,需要在redis中实现分布式锁。
解释:如果后端采用微服务架构,不使用任何分布式锁机制(mysql、zookeeper、redis)
电商网站中的秒杀,拿到库存判断,然后在减库存,双11之前,为了减少DB的压力,挡住第一批最大秒杀,把库存预热到了KV,现在KV的库存是1。服务A去Redis查询到库存发现是1,那说明我能抢到这个商品对不对,那我就准备减一了,但是还没减。同时服务B也去拿发现也是1,那我也抢到了呀,那我也减。C同理。等所有的服务都判断完了,你发现诶,怎么变成-2了,超卖了呀,这下完了。
4.1.3 zookeeper五个用途 + zookeeper存储 + zookeeper四种节点创建
zookeeper开发中的使用场景/zookeeper有什么用:
开发用途1:服务注册与订阅(共用节点,类似于eureka,作为注册中心)
开发用途2:分布式通知(使用 监听znode 实现)
开发用途3:服务命名(使用 znode特性 实现)
开发用途4:数据订阅、发布(使用 watcher 实现)
开发用途5:分布式锁(使用 临时顺序节点 实现,三种分布式锁:mysql zookeeper )
金手指:zookeeper存储
问题1:zookeeper是什么?
回答1:zookeeper是个数据库,文件存储系统,并且有监听通知机制(观察者模式)
问题2:接上面,zookeeper作为一个类Unix文件系统,它是如何存储数据的?
回答2:zookeeper通过节点来存储数据。
问题3:接上面,zookeeper节点?
回答3:zk的节点类型有4大类:持久化节点(zk断开节点还在)、持久化顺序编号目录节点、临时目录节点(客户端断开后节点就删除了)、临时目录编号目录节点。
注意:节点名称都是唯一的,这是zookeeper实现分布式锁互斥的基石。
使用层面:zookeeper节点怎么创建(linux操作)?
create /test laogong // 创建永久节点
create -e /test laogong // 创建临时节点
create -s /test // 创建顺序节点
create -e -s /test // 创建临时顺序节点
临时节点与永久节点:退出后,重新连接,上一次创建的所有临时节点都没了,但是永久节点还有。
4.2 zookeeper是如何基于节点去实现各种分布式锁的?
总问题:zookeeper是如何基于节点去实现各种分布式锁的?
问题1:节点,zookeeper如何实现分布式锁?
回答1:和lock一样的,在子节点逻辑之前创建节点,在finally块中释放节点,两行代码实现分布式锁。
解释1:
(1)在zookeeper中,节点名为唯一的,给定节点名创建一个后就不能再创建一个,会报错,创建指定节点名的节点,类似于获得到了锁;
(2)与lock不同的是,lock是一个进程内,对所有线程可见,而节点名是对 分布式部署 中所有的节点可见;
(3)正常情况下,会执行finally的语句,即表示节点执行完逻辑后,正常释放节点。
五、小结
zookeeper分布式锁,完成了。
天天打码,天天进步!!!