• Spring Boot整合Zookeeper实现分布式锁


    目录

    温馨提示本篇文章要求掌握zk的数据结构,以及临时序号节点!

    zk实现分布式锁完全是依靠zk节点类型当中的临时序号节点来实现的

    文章部分知识来源千峰教育讲解的zookeeper当中!

    一、Java当中关于锁的概念

    1.1.什么是锁

    锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。

    1.2.锁的使用场景

    以减库存为例,库存这时候就剩1个,那么我们得保证只会有1个请求真正的完成减1操作,假如代码逻辑是,先从库里查库存,通过if条件判断,如果有就减,没有就返回购买失败。

    这时候并发访问减库存接口,可能这时候我们代码当中的if判断已经失效了,多个请求同时查到还有一个,并且已经进入了if判断,没有加锁的话,这时候事情就比较严重了,库存一下子就成了负数。

    对于这块代码我们不希望同时有人减库存,这时候就需要加锁来控制,加完锁之后,就是将并发请求改成了串行,也就是不管并发了多少个请求,我通过加锁,只让1个请求进行减库存,你们去争抢锁资源吧,谁先抢到了就是谁的。

    1.3.什么是分布式锁

    分布式锁是控制 微服务集群 之间同步访问共享资源的一种方式。

    1.4.分布式锁的使用场景

    使用分布式锁前提:微服务一定是集群,这里的微服务不是指的zk,而是指的我们的业务模块,在项目当中一般的,由于并发量较高,往往会将业务拆分为一个模块一个模块,例如订单模块,库存模块,拆模块其中一个目的就是为了针对于模块的并发性进行集群部署,比如订单模块用的比较多,我可以搭建多个订单模块,但是尽管他们的模块是多个,围绕的数据还是共通的(一个数据库)。

    集群情况下,我们在代码加普通锁已经解决不了问题,假如现在有三个库存微服务,设置了负载均衡的方式访问,普通锁只能控制自己的服务减库存不会出现负数,但是他控制不了其他两个服务,数据是共通的,所以这时候只能使用分布式锁,通过分布式锁来控制三个服务当库存只有一个商品的时候,只能有一个服务访问的请求可以减库存成功。

    二、zk实现分布式锁

    2.1.zk中锁的种类:

    • 读锁 :创建⼀个临时序号节点,节点的名称会包含READ的字母,表示是读锁,⼤家都可以读,要想上读锁的前提: 之前的锁没有写锁
    • 写锁 :创建⼀个临时序号节点,节点的名称会包含WRIT的字母,表示是写锁,只有得到写锁的才能写。要想上写锁的前提是, 之前没有任何锁

    读锁和写锁完全是按照创建的临时序号节点的名称来区分的!

    • 序号节点创建出的节点,根据先后顺序,会在节点之后带上⼀个数值,越往后执⾏数值越⼤,类似于mysql的主键自增
    • 临时节点 :临时节点是在会话结束后,⾃动被删除的,通过这个特性,zk可以实现服务注册与发现的效果。假如会话关掉后大概10s左右,创建的临时节点就会消失。这个会话就是指的连接zk的客户端。
    • 临时序号节点 :就是上面两个的结合体

    当需要上锁的时候,就进行创建临时序号节点,释放锁的时候就删除节点。

    2.2.zk如何上读锁

    • 创建⼀个临时序号节点
    • 获取当前zk中序号⽐⾃⼰⼩的所有节点
    • 判断最⼩节点是否是读锁:
      • 如果不是读锁的话,则上锁失败,为最⼩节点设置监听。阻塞等待,zk的watch机制会当最⼩节点发⽣变化时通知当前节点,于是再执⾏第⼆步的流程
      • 如果是读锁的话,则上锁成功

    想要上读锁,主要就是需要看比他小的节点当中是否有写锁。如果有写锁,就需要等他用完之后删除节点,通过watch机制来通知他,写锁已经释放,然后他再进行第二步判断。

    2.3.zk如何上写锁

    • 创建⼀个临时序号节点
    • 获取zk中所有的⼦节点
    • 判断⾃⼰是否是最⼩的节点:
      • 如果是,则上写锁成功
      • 如果不是,说明前⾯还有锁,则上锁失败,监听最⼩的节点,如果最⼩节点有变化,则回到第⼆步。

     

    2.4.⽺群效应

    如果⽤上述的上锁⽅式,只要有节点发⽣变化,就会触发其他节点的监听事件,这样的话对zk的压⼒⾮常⼤,——⽺群效应。可以调整成链式监听。解决这个问题。

    假如并发了100个请求都需要获取写锁,这时候创建了100个节点来监听最小节点,当最小节点发生变化的时候,意味着他一下子要进行通知100个节点,zk瞬间会压力非常大。

    所以这时候可以采用链式监听,链式监听仍然是依靠的序号节点的特点。就好比mysql设置自增后,不管多少并发请求,他仍然能保证id的唯一性,zk的序号节点同样也是。让他们都不再监听最小节点,而是监听他的上一个节点。当上一个节点释放锁后,那当前节点就可以创建写锁了。

    这里还会遇到一个问题,假如他的上一个节点意外删除了,但是并不是等着拿到锁后释放锁,而是单纯的不想等了,所以删除了节点,而上面还有很多节点加着锁呢,所以我们不能单纯的靠上一个节点删除后当前节点就进行加锁。我们加写锁要保证的是,他上面没有任何节点加锁。这时候让他进行监听上上个节点即可。假如上上个节点仍然有问题了,那就监听上上上个节点。 总之一点就是尽量避免不让多个节点同时去监听一个节点

    三、springboot整合分布式锁

    springboot整合curator客户端: https://blog.csdn.net/weixin_43888891/article/details/125442668

    我直接是基于上一篇文章当中的项目进行 分布式锁 练习的!

    根据上面提到的zk分布式锁实现思路,我们其实并不用去自己写,在curator客户端已经给我们提供了现成的方法,我们只需要简单的调用客户端提供的方法,就可以实现分布式锁功能!

    1. <pre class="prettyprint hljs java">@Autowired
    2. CuratorFramework curatorFramework;
    3. /**
    4. * 获取读锁的条件是前面没有写锁
    5. * 当/lock1节点不存在的时候,我们不需要手动去创建,获取锁的时候会自动创建
    6. * 自动创建的是临时节点,用完之后释放锁的时候会删除掉的
    7. * 获取到锁之后会在/lock1节点下创建一个临时序号节点
    8. * 然后没有获取到锁的线程也会创建一个节点,这时候处于等待期间
    9. * 释放锁的时候,首先会删除掉自己的序号节点,然后假如没有人在排队用锁,这时候会把/lock1节点也删除掉
    10. *
    11. * @throws Exception
    12. */
    13. @Test
    14. void testGetReadLock() throws Exception {
    15. // 读写锁
    16. InterProcessReadWriteLock interProcessReadWriteLock = new
    17. InterProcessReadWriteLock(curatorFramework, "/lock1");
    18. // 获取读锁对象(创建对象耗时也就18毫秒)
    19. InterProcessLock interProcessLock = interProcessReadWriteLock.readLock();
    20. System.out.println("等待获取读锁对象!");
    21. // 获取锁(假如一直没拿到锁这个方法一直会是阻塞的,就算不阻塞的情况下,这个方法耗时也是特别长,高达17秒)
    22. interProcessLock.acquire();
    23. // 正常的我们代码假如走到了这一步,说明已经获取到锁了,这里写相关的业务代码即可,执行完记住释放锁
    24. System.out.println("获取到了锁!");
    25. for (int i = 1; i <= 100; i++) {
    26. Thread.sleep(3000);
    27. System.out.println(i);
    28. }
    29. // 释放锁(方法耗时13毫秒)
    30. interProcessLock.release();
    31. // 走到这一步的时候节点已经被删除了
    32. System.out.println("等待释放锁!");
    33. }
    34. /**
    35. * 获取写锁的条件是前面没有任何的锁
    36. *
    37. * @throws Exception
    38. */
    39. @Test
    40. void testGetWriteLock() throws Exception {
    41. // 读写锁
    42. InterProcessReadWriteLock interProcessReadWriteLock = new
    43. InterProcessReadWriteLock(curatorFramework, "/lock1");
    44. // 获取写锁对象
    45. InterProcessLock
    46. interProcessLock = interProcessReadWriteLock.writeLock();
    47. System.out.println("等待获取写锁对象!");
    48. // 获取锁(假如一直没拿到锁这个方法一直会是阻塞的)
    49. interProcessLock.acquire();
    50. for (int i = 1; i <= 100; i++) {
    51. Thread.sleep(3000);
    52. System.out.println(i);
    53. }
    54. // 释放锁
    55. interProcessLock.release();
    56. System.out.println("等待释放锁!");
    57. }
    58. /**
    59. * 方便测试多个线程获取读锁
    60. *
    61. * @throws Exception
    62. */
    63. @Test
    64. void testGetReadLock1() throws Exception {
    65. testGetReadLock();
    66. }</pre>

    这个是两个获取读锁的时候的节点场景:

     

    这个是一个获取读锁,一个获取写锁的场景,然后读锁节点是0004,所以是后创建的,他只有等待0003释放写锁,才能获取到读锁。

    zk分布式锁会产生死锁吗?

    这个肯定是不会的,因为假如有客户端拿到了锁,还没释放,服务挂了,这时候依据临时节点的特性,当临时节点和客户端断开连接几秒后会自动删除的,删除节点也就意味着自动释放锁!

    正常情况不建议使用zk作为分布式锁,效率属实太慢。

  • 相关阅读:
    pt25django教程
    PDF怎么编辑文字?教你两招非常实用的方法
    Hive存储格式之ORC File详解,什么是ORC File
    武汉光华芯CJC5340 ADC 数字音频系统模拟数字转换器
    Springboot 全局异常、返回实体配置
    Android Kotlin 基础详解
    VPN(虚拟专用网)攻略大全,你一定会用到!
    mysql 临时文件过大
    Redis 的内存淘汰策略和过期删除策略,你别再搞混了!
    u盘刻录系统安装盘
  • 原文地址:https://blog.csdn.net/jjc4261/article/details/125503730