分布式锁面试官问题
- zookeeper实现过分布式锁吗?
 - 说说你用redis实现过分布式锁吗?如何实现的?
 - 分布式情况下,如何解决高并发订单号重复问题
 
什么是分布式锁
集群环境下保证ID不重复
原来的Synchronized + Lock只能锁单机,也就是只能在一个JVM环境下
1:N关系
表示以前单机情况下,1个jvm虚拟机跑N个订单线程(这是个例子),在并发下,可以保证只有一个订单线程,拿到一个订单号,其余给锁住了
而在分布式+集群的环境下,变成了N对N的关系,在并发的环境下,如果使用UUID或者自增ID,就可能出现ID重复的问题,因此在集群下的环境下,对JVM进行加锁,这就是分布式锁。
分布式锁: 对多个jvm,同一时间只运行一个jvm运行某个业务的进程,其余相同的进程被锁住
比如jvm相当于教室,现在6个教室,要求采集学生信息同一时间只能一个教室的去(分布式锁)
分布式锁设计思想
针对小厂:qps < 2000
方案1:mysql的乐观锁 (了解即可,实际不可能)
方案2:redis(redission)
方案3:zookeeper
Zookeeper实现分布式锁
因为Zookeeper在创建节点的时候,需要保证节点的唯一性,也就是实现原理就是,每次一个线程获取到了锁,那就在Zookeeper上创建一个临时节点,但用完锁之后,在把这个节点删除掉
create /node v0410 # 创建一个持久节点crate -e /node v0410 # 创建一个临时节点
对于单进程的并发场景,我们可以使用synchronized关键字和Reentrantlock等
对于 分布式场景,我们可以使用分布式锁。
创建锁
多个JVM服务器之间,同时在zookeeper上创建相同一个临时节点,因为临时节点路径是保证唯一,只要谁能创建节点成功,谁就能获取到锁。
没有创建成功节点,只能注册个监听器监听这个锁并进行等待,当释放锁的时候,采用事件通知给其它客户端重新获取锁的资源。
这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入获取锁的步骤。
释放锁
Zookeeper使用直接关闭临时节点session会话连接,因为临时节点生命周期与session会话绑定在一块,如果session会话连接关闭,该临时节点也会被删除,这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入到获取锁的步骤。
zookeeper单机环境下的锁
假设我们现在有一个订单ID生成的工具类
/*** Created by gysui on 2020/11/23*/public class OrderNumberCreateUtil {private static int num = 0;public String getOrderNumber() {return "\t 生成订单号:" + (++num);}}
然后有一个OrderService服务类
/*** Created by gysui on 2020/11/23*/public class OrderServiceImpl implements OrderService {private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();public void getOrderNumber() {System.out.println(Thread.currentThread().getName() +orderNumberCreateUtil.getOrderNumber());}}
当我们客户端有50个线程进行访问获取订单号的时候
0 生成订单号:110 生成订单号:109 生成订单号:96 生成订单号:88 生成订单号:77 生成订单号:65 生成订单号:51 生成订单号:13 生成订单号:34 生成订单号:42 生成订单号:215 生成订单号:1514 生成订单号:1413 生成订单号:1312 生成订单号:1211 生成订单号:1116 生成订单号:1618 生成订单号:1717 生成订单号:1819 生成订单号:1920 生成订单号:2021 生成订单号:2122 生成订单号:2223 生成订单号:2324 生成订单号:2425 生成订单号:2526 生成订单号:2627 生成订单号:2728 生成订单号:2829 生成订单号:2930 生成订单号:3031 生成订单号:3132 生成订单号:3233 生成订单号:3334 生成订单号:3435 生成订单号:3536 生成订单号:3638 生成订单号:3739 生成订单号:3842 生成订单号:3937 生成订单号:4043 生成订单号:4141 生成订单号:4244 生成订单号:4440 生成订单号:4346 生成订单号:4549 生成订单号:4947 生成订单号:4848 生成订单号:4745 生成订单号:46
解决方案是,在Service下,加锁解决
/*** Created by gysui on 2020/11/23*/public class OrderServiceImpl implements OrderService {private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();public void getOrderNumber() {Lock lock = new ReentrantLock();lock.lock();try {System.out.println(Thread.currentThread().getName() +orderNumberCreateUtil.getOrderNumber());} finally {lock.unlock();}}}
分布式环境下的锁
模板模式
概念
在模板模式(Template Pattern)设计模式中,用一个抽象类公开定义了执行它的方法的方式、模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构,即可重定义该算法的某些特定步骤:
主要解决:一些方法通用,却在每个子类都重新写了这一方法
何时使用:在一些通用的方法
如何解决:将这些通用算法抽象出来
关键代码:在抽象父类中实现通用方法,其它步骤下放到子类中实现
应用实例
西游记里面菩萨定义好了81难,不管是女儿国,或者蜘蛛精,只需要有81劫难,这就是一个顶层的逻辑骨架
spring中对Hibernate的支持,将一些定好的方法封装起来,比如开启事务,获取Session,关闭Session,程序要不需要重复写那些已经规范好的代码,直接丢一个实体就可以保存。
优缺点
- 封装不变部分,扩展可变部分
 - 提取公共代码,便于维护
 - 行为由父类控制,子类实现
 - 缺点是:每一个不同的实现,都需要一个子类来实现,导致类的个数增加,使得系统变庞大
 
使用场景
- 有很多子类共有的方法,且逻辑相同
 - 重要的、复杂的方法,可以考虑模板方法
 
代码
例如,我们首先定义一个ILock的接口
/*** Created by gysui on 2020/11/23*/public interface ILock {/*** 上锁,* 首先会尝试加锁,如果加锁失败说明当前锁被占用* 开始等待锁释放,如果一旦检测到锁被释放就递归该方法,再次尝试加锁*/void lock();/*** 解锁*/void unlock();}
然后在抽象类中继承该接口,同时实现Lock 和 unLock的方法
/*** Created by gysui on 2020/11/23* 锁的模板类*/public abstract class LockTemplate implements ILock {@Overridepublic void lock() {// 尝试加锁if (tryLock()) {System.out.println(Thread.currentThread().getName() + "获取到锁");} else {// 等待释放锁waitLock();// 递归 locklock();}}@Overridepublic void unlock() {close();}/*** 释放锁*/public abstract void close();/*** 尝试加锁*/protected abstract boolean tryLock();/*** 等待锁被释放*/protected abstract void waitLock();}
同时我们在抽象类里,又定义了3个抽象方法,waitLock() 和 tryLock 以及clone()
/*** Created by gysui on 2020/11/23*/public class ZkLock extends LockTemplate {protected ZkClient zkClient = new ZkClient("192.168.1.106:2181",6000);protected static final String PATH = "/lock";public ZkLock() {}@Overridepublic void close() {zkClient.close();}@Overrideprotected boolean tryLock() {// 判断节点是否存在,如果存在则返回false,否者返回truetry {zkClient.createEphemeral(PATH);return true;} catch (Exception e) {return false;}}@Overrideprotected void waitLock() {CountDownLatch countDownLatch = new CountDownLatch(1);IZkDataListener zkDataListener = new IZkDataListener() {@Overridepublic void handleDataChange(String s, Object o) throws Exception {}@Overridepublic void handleDataDeleted(String s) throws Exception {// 监控到锁被释放countDownLatch.countDown();}};// 等待锁的时候,需要加监控,查询这个lock是否释放zkClient.subscribeDataChanges(PATH,zkDataListener);try {// 程序会阻塞在该处,直到 1 减少到 0countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}}}
然后我们通过ILock进行加锁
/**
 * Created by gysui on 2020/11/23
 */
public class OrderServiceImpl implements OrderService {
    private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();
    public void getOrderNumber()  {
        ILock lock = new ZkLock();
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName() +
                    orderNumberCreateUtil.getOrderNumber());
        } finally {
            lock.unlock();
        }
    }
}
然后在使用多个线程进行操作,而且是在线程里面实例化对象,来进行创建,最终保证每个对象再获取订单的时候,都是唯一的
/**
 * Created by gysui on 2020/11/23
 * 分布式锁
 */
public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                new OrderServiceImpl().getOrderNumber();
            },String.valueOf(i)).start();
        }
    }
}
运行结果
44获取到锁
44     生成订单号:1
46获取到锁
46     生成订单号:2
39获取到锁
39     生成订单号:3
40获取到锁
40     生成订单号:4
17获取到锁
17     生成订单号:5
6获取到锁
6     生成订单号:6
29获取到锁
29     生成订单号:7
13获取到锁
13     生成订单号:8
0获取到锁
0     生成订单号:9
28获取到锁
28     生成订单号:10
36获取到锁
36     生成订单号:11
34获取到锁
34     生成订单号:12
31获取到锁
31     生成订单号:13
10获取到锁
10     生成订单号:14
2获取到锁
2     生成订单号:15
21获取到锁
21     生成订单号:16
26获取到锁
26     生成订单号:17
38获取到锁
38     生成订单号:18
32获取到锁
32     生成订单号:19
35获取到锁
35     生成订单号:20
43获取到锁
43     生成订单号:21
23获取到锁
23     生成订单号:22
9获取到锁
9     生成订单号:23
12获取到锁
12     生成订单号:24
47获取到锁
47     生成订单号:25
49获取到锁
49     生成订单号:26
41获取到锁
41     生成订单号:27
14获取到锁
14     生成订单号:28
8获取到锁
8     生成订单号:29
42获取到锁
42     生成订单号:30
18获取到锁
18     生成订单号:31
37获取到锁
37     生成订单号:32
30获取到锁
30     生成订单号:33
48获取到锁
48     生成订单号:34
16获取到锁
16     生成订单号:35
33获取到锁
33     生成订单号:36
25获取到锁
25     生成订单号:37
3获取到锁
3     生成订单号:38
7获取到锁
7     生成订单号:39
4获取到锁
4     生成订单号:40
20获取到锁
20     生成订单号:41
1获取到锁
1     生成订单号:42
11获取到锁
11     生成订单号:43
15获取到锁
15     生成订单号:44
19获取到锁
19     生成订单号:45
27获取到锁
27     生成订单号:46
22获取到锁
22     生成订单号:47
45获取到锁
45     生成订单号:48
24获取到锁
24     生成订单号:49
5获取到锁
5     生成订单号:50
                    