分布式锁面试官问题

  1. zookeeper实现过分布式锁吗?
  2. 说说你用redis实现过分布式锁吗?如何实现的?
  3. 分布式情况下,如何解决高并发订单号重复问题

什么是分布式锁

集群环境下保证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上创建一个临时节点,但用完锁之后,在把这个节点删除掉

  1. create /node v0410 # 创建一个持久节点
  2. crate -e /node v0410 # 创建一个临时节点

对于单进程的并发场景,我们可以使用synchronized关键字和Reentrantlock等

对于 分布式场景,我们可以使用分布式锁。

创建锁

多个JVM服务器之间,同时在zookeeper上创建相同一个临时节点,因为临时节点路径是保证唯一,只要谁能创建节点成功,谁就能获取到锁。

没有创建成功节点,只能注册个监听器监听这个锁并进行等待,当释放锁的时候,采用事件通知给其它客户端重新获取锁的资源。

这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入获取锁的步骤。

释放锁

Zookeeper使用直接关闭临时节点session会话连接,因为临时节点生命周期与session会话绑定在一块,如果session会话连接关闭,该临时节点也会被删除,这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入到获取锁的步骤。

zookeeper单机环境下的锁

假设我们现在有一个订单ID生成的工具类

  1. /**
  2. * Created by gysui on 2020/11/23
  3. */
  4. public class OrderNumberCreateUtil {
  5. private static int num = 0;
  6. public String getOrderNumber() {
  7. return "\t 生成订单号:" + (++num);
  8. }
  9. }

然后有一个OrderService服务类

  1. /**
  2. * Created by gysui on 2020/11/23
  3. */
  4. public class OrderServiceImpl implements OrderService {
  5. private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();
  6. public void getOrderNumber() {
  7. System.out.println(Thread.currentThread().getName() +
  8. orderNumberCreateUtil.getOrderNumber());
  9. }
  10. }

当我们客户端有50个线程进行访问获取订单号的时候

  1. 0 生成订单号:1
  2. 10 生成订单号:10
  3. 9 生成订单号:9
  4. 6 生成订单号:8
  5. 8 生成订单号:7
  6. 7 生成订单号:6
  7. 5 生成订单号:5
  8. 1 生成订单号:1
  9. 3 生成订单号:3
  10. 4 生成订单号:4
  11. 2 生成订单号:2
  12. 15 生成订单号:15
  13. 14 生成订单号:14
  14. 13 生成订单号:13
  15. 12 生成订单号:12
  16. 11 生成订单号:11
  17. 16 生成订单号:16
  18. 18 生成订单号:17
  19. 17 生成订单号:18
  20. 19 生成订单号:19
  21. 20 生成订单号:20
  22. 21 生成订单号:21
  23. 22 生成订单号:22
  24. 23 生成订单号:23
  25. 24 生成订单号:24
  26. 25 生成订单号:25
  27. 26 生成订单号:26
  28. 27 生成订单号:27
  29. 28 生成订单号:28
  30. 29 生成订单号:29
  31. 30 生成订单号:30
  32. 31 生成订单号:31
  33. 32 生成订单号:32
  34. 33 生成订单号:33
  35. 34 生成订单号:34
  36. 35 生成订单号:35
  37. 36 生成订单号:36
  38. 38 生成订单号:37
  39. 39 生成订单号:38
  40. 42 生成订单号:39
  41. 37 生成订单号:40
  42. 43 生成订单号:41
  43. 41 生成订单号:42
  44. 44 生成订单号:44
  45. 40 生成订单号:43
  46. 46 生成订单号:45
  47. 49 生成订单号:49
  48. 47 生成订单号:48
  49. 48 生成订单号:47
  50. 45 生成订单号:46

解决方案是,在Service下,加锁解决

  1. /**
  2. * Created by gysui on 2020/11/23
  3. */
  4. public class OrderServiceImpl implements OrderService {
  5. private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();
  6. public void getOrderNumber() {
  7. Lock lock = new ReentrantLock();
  8. lock.lock();
  9. try {
  10. System.out.println(Thread.currentThread().getName() +
  11. orderNumberCreateUtil.getOrderNumber());
  12. } finally {
  13. lock.unlock();
  14. }
  15. }
  16. }

分布式环境下的锁

模板模式

概念

在模板模式(Template Pattern)设计模式中,用一个抽象类公开定义了执行它的方法的方式、模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行

意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构,即可重定义该算法的某些特定步骤:

主要解决:一些方法通用,却在每个子类都重新写了这一方法

何时使用:在一些通用的方法

如何解决:将这些通用算法抽象出来

关键代码:在抽象父类中实现通用方法,其它步骤下放到子类中实现

应用实例

西游记里面菩萨定义好了81难,不管是女儿国,或者蜘蛛精,只需要有81劫难,这就是一个顶层的逻辑骨架
spring中对Hibernate的支持,将一些定好的方法封装起来,比如开启事务,获取Session,关闭Session,程序要不需要重复写那些已经规范好的代码,直接丢一个实体就可以保存。

优缺点

  • 封装不变部分,扩展可变部分
  • 提取公共代码,便于维护
  • 行为由父类控制,子类实现
  • 缺点是:每一个不同的实现,都需要一个子类来实现,导致类的个数增加,使得系统变庞大

使用场景

  • 有很多子类共有的方法,且逻辑相同
  • 重要的、复杂的方法,可以考虑模板方法

代码

例如,我们首先定义一个ILock的接口

  1. /**
  2. * Created by gysui on 2020/11/23
  3. */
  4. public interface ILock {
  5. /**
  6. * 上锁,
  7. * 首先会尝试加锁,如果加锁失败说明当前锁被占用
  8. * 开始等待锁释放,如果一旦检测到锁被释放就递归该方法,再次尝试加锁
  9. */
  10. void lock();
  11. /**
  12. * 解锁
  13. */
  14. void unlock();
  15. }

然后在抽象类中继承该接口,同时实现Lock 和 unLock的方法

  1. /**
  2. * Created by gysui on 2020/11/23
  3. * 锁的模板类
  4. */
  5. public abstract class LockTemplate implements ILock {
  6. @Override
  7. public void lock() {
  8. // 尝试加锁
  9. if (tryLock()) {
  10. System.out.println(Thread.currentThread().getName() + "获取到锁");
  11. } else {
  12. // 等待释放锁
  13. waitLock();
  14. // 递归 lock
  15. lock();
  16. }
  17. }
  18. @Override
  19. public void unlock() {
  20. close();
  21. }
  22. /**
  23. * 释放锁
  24. */
  25. public abstract void close();
  26. /**
  27. * 尝试加锁
  28. */
  29. protected abstract boolean tryLock();
  30. /**
  31. * 等待锁被释放
  32. */
  33. protected abstract void waitLock();
  34. }

同时我们在抽象类里,又定义了3个抽象方法,waitLock() 和 tryLock 以及clone()

  1. /**
  2. * Created by gysui on 2020/11/23
  3. */
  4. public class ZkLock extends LockTemplate {
  5. protected ZkClient zkClient = new ZkClient("192.168.1.106:2181",6000);
  6. protected static final String PATH = "/lock";
  7. public ZkLock() {
  8. }
  9. @Override
  10. public void close() {
  11. zkClient.close();
  12. }
  13. @Override
  14. protected boolean tryLock() {
  15. // 判断节点是否存在,如果存在则返回false,否者返回true
  16. try {
  17. zkClient.createEphemeral(PATH);
  18. return true;
  19. } catch (Exception e) {
  20. return false;
  21. }
  22. }
  23. @Override
  24. protected void waitLock() {
  25. CountDownLatch countDownLatch = new CountDownLatch(1);
  26. IZkDataListener zkDataListener = new IZkDataListener() {
  27. @Override
  28. public void handleDataChange(String s, Object o) throws Exception {
  29. }
  30. @Override
  31. public void handleDataDeleted(String s) throws Exception {
  32. // 监控到锁被释放
  33. countDownLatch.countDown();
  34. }
  35. };
  36. // 等待锁的时候,需要加监控,查询这个lock是否释放
  37. zkClient.subscribeDataChanges(PATH,zkDataListener);
  38. try {
  39. // 程序会阻塞在该处,直到 1 减少到 0
  40. countDownLatch.await();
  41. } catch (InterruptedException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. }

然后我们通过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