学会Zookeeper分布式锁--让面试官对你刮目相看

业务场景:

电商系统,用户下单,需要生成唯一的订单编号,并且需要有业务意义,而不能使用UUID这种字符串,比如:

年-月-日-时-分-秒-自增序号

2019-11-11-23-59-59-001

订单编号生成器工具类OrderCodeGenerator

/** * @description: 订单编号生成器 * @author: stwen_gan * @date: **/public class OrderCodeGenerator {    /** 自增序列  **/    private int i = 0;    /**     * 根据当前时间生成订单编号:"yyyy-MM-dd-HH-mm-ss-自增序号"     * @return     */    public String getOrderCode(){        Date date  = new Date();        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-");        return simpleDateFormat.format(date)+ ++i;    }}

创建订单服务

@Slf4jpublic class OrderServiceImpl implements OrderService {    private OrderCodeGenerator codeGenerator = new OrderCodeGenerator();    /**     * 创建订单     * @return     */    @Override    public String createOrder() {        //生成订单编号        String orderCode = codeGenerator.getOrderCode();        log.info(Thread.currentThread().getName()+"-->生成订单编号:{}",orderCode);        // TODO 具体写自己的生成订单业务        return orderCode;    }}

一、单机环境

服务部署一个实例,并发情况,订单编号能唯一吗?

如何模拟并发:

模拟20个并发线程,使用for循环创建20个线程,使用CyclicBarrier .await() 并发协调控制,使20个线程全创建好之后,再同时一起启动往下执行!(循环屏障)

模拟并发测试 ConcurrentTest

@Slf4jpublic class ConcurrentTest {    public static void main(String[] args) {        //并发线程数        int count = 20;        //循环屏障        CyclicBarrier cb  = new CyclicBarrier(count);        OrderService orderService = new OrderServiceImpl();        //模拟高并发场景,创建订单        for(int i=0; i            new Thread(new Runnable() {                @Override                public void run() {                    log.info(Thread.currentThread().getName()+"--我已经准备好了");                    try {                        //等待所有线程启动准备好,才一起往下执行                        cb.await();                    } catch (InterruptedException | BrokenBarrierException e) {                        e.printStackTrace();                    }                    //创建订单                    orderService.createOrder();                }            }).start();        }    }}

运行该测试类main方法,控制台如下:

可以看到,模拟的20个人同时并发下单,生成的订单编号有重复:

解决方案一:在订单服务类,调用订单编号生成方法时,加上Lock锁(可重入锁),并发情况,只有获取到锁的才可以进去调用。

改进订单服务:

加了Lock锁优化后,变成如下场景:

再次运行刚才的测试类,生成的工单编号没有重复了。

但是假如,有上万的并发,单台服务无法支撑怎么办?

---加服务器,部署集群!!!

二、集群环境

订单服务部署集群,并发下单,此时订单编号能唯一吗?

场景如下:

做集群,如何模拟验证?

是不是要部署多台tomcat??

之前模拟并发测试类,订单服务是放在for外面,如下:

修改:将订单服务实例,移到创建线程的for循环里,每个线程都会创建调用一个订单服务,从而模拟集群场景(这里模拟20个线程,则模拟20个tomcat):

再次运行测试类,结果如下:

订单编号的末尾序号最后全是1,为什么?

--> 因为模拟了20个tomcat(每个线程分别创建了属于自己的orderService订单服务实例),每个都调用不同的编号生成实例,相当于每个tomcat都有一个编号生成器,每个都从1开始。如下:

再次改进方案:

将订单编号生成器类单独抽取一个tomcat,使多个tomcat调用同一个编号生成器服务

如何模拟验证?

在订单编号服务调用类中,将订单编号生成器类实例,设为static 静态变量,则他们调用的都是同一个实例。

此时订单编号能唯一吗?

把并发数收到50或100,尽量大点,看出效果:

为什么还会出现重复编号?

因为每个tomcat都有自己各自的锁,不是同一把锁!如下:

那我们再把锁也加上static,锁也是唯一实例了,可以吗?

刚才,我们上面模拟的只是,每个tomcat只分配一个订单请求的情况,如下场景:

-->还是不可以,因为,假如tomcat1有20个并发,它们用的的确是同一把锁(同一个jvm下,static锁),而tomcat2也有20个并发,它们用的却是另外一把锁...

所以,使用jvm锁,无法解决集群并发问题!!!!

再再改进方案:

我们把生成订单编号独立成共享服务,并且把锁加在编号服务上(把锁在共享编号服务后面),如下:

此时,并发情况,订单编号还会出现重复吗?

后面加锁,虽然订单编号不会重复了,但是,大并发情况,阻塞卡死在后面单台服务器(假如后面订单编号服务也部署集群,那么又会出现编号重复)。

因此,需要使用分布式锁!

终于进入本文的正题了  --->别走开,后面更精彩0.0

 

三、Zookeeper分布式锁

1、分布式锁,场景描述:

分布式锁用途:在分布式环境下协同共享资源的使用。

2、分布式锁思路分析

锁特点:

  • 排他性:同一时间,只有一个线程能获得;

  • 阻塞性:其它未抢到的线程阻塞等待,直到锁被释放,再继续抢;

  • 可重入性:线程获得锁后,后续是否可重复获取该锁(避免死锁)。

当然,还要考虑性能开销等问题。

3、常规的分布式锁解决方案有哪几种:

  • 文件系统:同一个目录下,不能存在同名文件

  • 数据库锁:主键 、 唯一约束  、for  update

  • 基于Redis的分布式锁:setnx、set、Redisson

  • 基于ZooKeeper的分布式锁:类似文件系统

对比分析:

  1. 使用数据库锁会有单机性能、单机故障等问题,锁没有失效时间,容易出现死锁,当然可以部署群集,也会出现各种各样的问题,性能开销高,这里不详细介绍。

  2. Redis缓存实现分布式锁,相对复杂,因为没有类似zk的watch监听通知机制,需要自己另外实现;而且Redis可能会出现死锁(或短时间内死锁),比如,获取到锁的线程挂了,必须等到该节点过期时间到了,才能删除。

  3. 而Zookeeper分布式锁可靠性比Redis好,实现相对简单,但由于需要创建节点、删除节点等,所有效率相比Redis要低。

那我们在实际项目中如何选择呢?

原则上如果并发量不是特别大,追求可靠性,那么首选zookeeper。而Redis实现的分布式锁响应更快,对并发的支持性能更好,如果为了效率,首选redis实现。

由于篇幅原因,本次仅介绍Zookeeper分布式锁解决方案,下次再介绍redis分布式锁方案。

4、Zookeeper简介

ZooKeeper是一个开源的的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

zk特点:

  • 类似文件目录的DataTree数据结构,可以在znode节点存取数据;内部是一个Map nodes的数据结构,其中key是path,DataNode才是真正保存数据的核心数据结构。

  • zk每个节点都有watcher监听通知机制,监听znode节点的增删改查的操作。

zk节点类型:

  • 持久节点 (persistent):节点创建后,就一直存在,直到主动删除。

  • 持久顺序节点 (persistent_sequential)

  • 临时节点 (ephemeral):客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper服务端会自动删除该节点。

  • 临时顺序节点 (ephemeral_sequential)

并且,同一个znode下,节点名称是唯一的。

5、zk服务安装:

官网下载zk包(windows),解压缩,如下:

演示注册watch监控节点变化:

(1)编写zk监听器测试类,双击运行zkServer.cmd,运行该类

(2)运行zkCli.cmd ,分别操作节点:/stwen/lock

控制台输出如下:

监听到该节点数据变化--->666监听到该节点数据变化--->888监听到该节点被删除--->/stwen/lock

6、zk分布式锁方案一

并发情况,都连接到ZK,先去ZK下创建子节点,

(1) 创建成功,则获得锁,用完释放锁,并通知其他订阅了该节点的线程;

(2) 假如创建节点失败,则注册该节点的watcher监听器(都去关注订阅那个节点),进入阻塞等待。

逻辑如下:

利用zk特性:同父的子节点不可以重名。

思考:

(1)用持久节点可以吗?

-->假如拥有锁的线程挂了呢?岂不是出现死锁...

(2)用临时节点呢?

-->使用临时节点,它有个特性,只要zk客户端与zk服务端失去连接,该节点就会被删除,那么其他注册了该节点watcher监听的线程则都会收到通知,去抢锁,不会死锁。

步骤:

引入ZKClient依赖

                    com.101tec            zkclient            0.11        
  1. 创建一个自己的ZKDistributeLock,实现Lock接口,复写它的tryLock()、lock()、unlock()等方法;

  2. tryLock()方法调用ZK的接口api,去给定节点下创建临时节点,创建成功,则获得锁,执行业务代码(去调用生成工单编号的服务),释放锁;

  3. tryLock()中创建临时节点失败,则注册一个watcher监听该节点的变化,并进入阻塞等待,当有节点释放锁时,会通知订阅了该watcher的线程,取消watcher监听,并尝试重新去创建临时节点,去抢锁。

  4. 多线程情况,调用前,先创建一个ZKDistributeLock,先获取锁,才能调用生成工单编号。

具体代码实现这里就不粘贴了,这种情况是肯定不会出现订单编号重复的。

但是:

假如有1万的并发,当其中一个获取到锁之后,释放锁时,同时就要通知9999个监听者,造成网络冲击等(惊群效应),最后也只能是只有一个能获取到锁!!!

在集群规模较大的环境中带来危害:

  • 巨大的服务器性能损耗

  • 网络冲击

  • 可能造成宕机

所以,终极方案:使用临时顺序节点实现zk分布式锁!
7、zk分布式锁方案二

使用临时顺序节点实现zk分布式锁,类似去银行办业务,取号排队(按顺序),上一个办完只会通知下一个。

逻辑如下:

步骤:

  1. 同样是创建一个自己的ZKDistributeLock,实现Lock接口,复写它的tryLock()、lock()、unlock()等方法;

  2. tryLock()方法调用ZK的接口api,尝试去获取锁,在给定节点下创建临时顺序节点,并获取子节点列表,然后判断当前创建的子节点序号是否比其他子节点小,如果是最小,则获得锁并执行业务代码,释放锁;

  3. tryLock()中如果对比其他子节点,自己创建的子节点并不是最小,则去对比自己小的节点注册watcher监听,只监听比自己上一个小的节点,进入阻塞等待。

  4. unlock()当释放锁时,watcher监听器会实时通知监听了它的节点(比他大一号的节点),触发他又重新获取子节点列表,并对比是否最小。。。

具体代码实现:

* 利用zk临时顺序节点实现分布式锁

* 获取锁:取排队号(创建自己的临时顺序节点),然后判断是否是最小号,则获得锁,不是则注册前一个节点的监听watcher,阻塞等待

* 释放锁:删除自己创建的临时节点

(1)创建一个分布式锁类 ZkDistributeLock,实现Lock接口,构造方法中初始化ZK连接,并实现lock()、unlock()、tryLock()等方法:

实现lock()

实现tryLock()方法

其中,waitGetLock()方法,如下:

(2)新增一个订单服务实现类

(3)将测试类中的订单服务换成上面的DisLockOrderServiceImpl

(4)运行测试类,控制台打印如下,没有出现重复订单编号

 Thread-5-->生成订单编号:2019-08-18-19-48-36-1 Thread-13-->生成订单编号:2019-08-18-19-48-36-2 Thread-18-->生成订单编号:2019-08-18-19-48-36-3 Thread-1-->生成订单编号:2019-08-18-19-48-36-4 Thread-8-->生成订单编号:2019-08-18-19-48-36-5 Thread-10-->生成订单编号:2019-08-18-19-48-36-6 Thread-14-->生成订单编号:2019-08-18-19-48-36-7 Thread-19-->生成订单编号:2019-08-18-19-48-36-8 Thread-9-->生成订单编号:2019-08-18-19-48-36-9 Thread-6-->生成订单编号:2019-08-18-19-48-36-10 Thread-16-->生成订单编号:2019-08-18-19-48-36-11 Thread-3-->生成订单编号:2019-08-18-19-48-36-12 Thread-2-->生成订单编号:2019-08-18-19-48-36-13 Thread-12-->生成订单编号:2019-08-18-19-48-36-14 Thread-0-->生成订单编号:2019-08-18-19-48-36-15 Thread-11-->生成订单编号:2019-08-18-19-48-36-16 Thread-4-->生成订单编号:2019-08-18-19-48-36-17 Thread-7-->生成订单编号:2019-08-18-19-48-36-18 Thread-17-->生成订单编号:2019-08-18-19-48-36-19 Thread-15-->生成订单编号:2019-08-18-19-48-36-20

当然,伴随着Zookeeper的流行,目前已经有基于zk的分布式锁实现框架了,Curator,它是Netflix开源的一套ZooKeeper客户端框架,它提供了zk场景的绝大部分实现,本文就不介绍Curator分布式锁的实现方案了,关键是要理解上面原理思路。

看完了本文,你掌握了zk分布式锁了吗 0.0

查看原文,请关注下如下微信公众号“阿甘正专”,定期有软文分享哦

参考:网易云课堂--微专业公开课

你可能感兴趣的:(Zookeeper,分布式锁,Zookeeper)