分布式学习之分布式锁原理与实现

  1. 分布式锁的应用场景
  2. 分布式锁的原理、实现方式
  3. 基于zookeeper的分布式锁实现

1 分布式锁的应用场景

业务:创建具有业务含义的订单编号。

在单机情况下,我们的实现:订单服务类 -> 订单编号生成类,一个订单服务对象对应一个订单编号生成对象。

问题:如果同时有很多人下单,订单号能否做到唯一?

此时多个线程访问,如果一个线程一个订单编号生产对象,则订单号肯定不唯一,需要将订单编号生成对象改为静态共享资源,在此基础上再进行加锁。

在高并发、大数据量的情况下,单台服务器已经无法撑起并发量,为了保证服务调用的效率,我们会进行集群部署。

此时,基于jvm的锁都无法保证生成的订单号是唯一的。

1.1 错误的加锁姿势

在单个服务器中对订单服务类进行加锁
分布式学习之分布式锁原理与实现
以上方案虽然对订单服务类加上了锁,但由于各个tomcat中使用的是各自的锁,所以订单号做不到唯一。

1.2 正确的加锁姿势

所有想要调用订单编号服务的service都必须争抢一把分布式锁,只有获得锁的service,才能接着调用订单编号服务。
分布式学习之分布式锁原理与实现
问题:为什么非要多此一举,不直接给订单编号生成服务加锁呢?

  • 为了避免单点故障
  • 进行访问控制,只有通过认证,被允许访问的服务才能调用订单编号生成服务

分布式锁的用途:在分布式环境下,协同共享资源的使用,在单机环境下,没有必要使用。

2 分布式锁的原理、实现方式

  1. 锁具有什么特点
  2. 常用分布式锁实现技术

2.1 锁具有什么特点

  • 排他性:只能有一个程序获得锁
  • 阻塞性:其它没有获得锁的程序需要阻塞等待
  • 可重入性:获得锁的进程,在执行过程中,可能会调用到其它加同一个锁的方法或函数,为了避免死锁出现,需要锁具有可重入性。

在现有的计算机技术中,具有排他性的有:文件系统、数据库(主键 唯一约束、for update排它锁)、缓存(redis setnx)和zookeeper(类文件系统)等。

2.2 常用分布式锁实现技术

2.2.1 基于数据库实现分布式锁
  • 性能较差,容易出现单点故障
  • 锁没有失效时间,容易死锁

数据库一般都是单实例的,且关系型数据库的数据都是结构化的,所以基于数据库实现的分布式锁性能会比较差。

2.2.2 基于缓存实现分布式锁
  • 实现复杂
  • 存在死锁(或短时间死锁)的可能

避免死锁的锁失效时间其实一直都是很难去定义的,跟当时的网络情况、执行时间是有关系的,如果网络较差,网络请求执行时间较长,则会出现业务没执行完就释放锁的情况。基于缓存的分布式锁,如redis的setnx设置了过期失效时间,但还是无法避免出现短时间死锁的情况。

2.2.3 基于zookeeper实现分布式锁
  • 实现相对简单
  • 可靠性高
  • 性能较好

zookeeper有高可靠、高性能的watcher机制,如果采用redis实现分布式锁,则需要自己实现唤醒订阅功能,且zookeeper类似于文件系统的节点树数据结构,同父的子节点不可重名的特性贴合我们对于锁排他性的要求。

3 基于zookeeper的分布式锁实现

  1. zookeeper简介
  2. zookeeper节点类型
  3. zookeeper典型应用场景
  4. 用zookeeper实现分布式锁逻辑一
  5. 改进后的实现数据结构和逻辑

3.1 zookeeper简介

  • 分布式的、开源的分布式应用程序协调服务

特性:节点树数据结构,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据。

3.2 zookeeper节点类型

  • 持久节点
  • 持久顺序节点
  • 临时节点
  • 临时顺序节点

3.3 zookeeper典型应用场景

  1. 数据发布订阅(配置中心)——分布式指挥官
  2. 命名服务
  3. Master选举
  4. 集群管理
  5. 分布式队列
  6. 分布式锁

3.4 用zookeeper实现分布式锁逻辑一

特性:zookeeper中同父的子节点不可重名

  • 加锁的逻辑

所有进程通过zk客户端连接zk,在zk的某个路径下边创建一个同名的临时节点,临时节点创建成功,即认为加锁成功,zookeeper类似于一个文件系统,是树形数据结构,在同一个目录下,不允许出现两个同名的子节点,所以有且仅有一个进程能够最终创建该临时节点。具体实现的时候,可以通过CountDownLatch的await()(Java)来进行阻塞等待,因为如果使用park/unpark机制需要知道具体阻塞的进程。

  • 解锁的逻辑

当某一进程获得锁,执行完业务后,开始释放锁,即删除该临时节点,由于临时节点过期自动删除的特性,所以就算该进程没有主动释放锁,也不会出现其他进程一直阻塞等待,造成死锁的情况。其他没有成功创建临时节点的进程,则在该节点路径**册watcher,一旦节点发生变动(被删除),则通知(唤醒)该进程,进程得到消息后,取消watcher,重新创建临时节点。
分布式学习之分布式锁原理与实现
该分布式锁实现方案(非公平锁,有些进程很早就过来等待锁了,却要跟所有进程一起争抢锁的占有权)在大规模并发的情况下,会出现惊群效应。就好比,我们在给池塘里的鱼喂面包,一小块面包屑丢下去,会有成群结队的鱼簇拥过来,但是最后有且只有一条鱼能够吃到这个面包屑,从结果导向来看,其它的鱼完全没有必要消耗体力游过来争抢。因此,我们释放锁的时候,不需要通知所有阻塞等待的节点。

惊群效应的危害

  • 巨大的服务器性能损耗
  • 网络冲击
  • 可能造成宕机

3.5 改进后的实现数据结构和逻辑

  • 所有争抢锁的进程,不再是创建一个同名的临时节点了,而是创建顺序临时节点,意味着所有节点的名字不一样,且按照从小到大的顺序分布在zookeeper中lock路径下。
  • 不会出现惊群效应,因为进程只需要监听前一个顺序临时节点的状态,当前一个节点退出等待或者被删除后,将通知后面的进程加锁,这就跟银行办理业务一样,所有的人都需要取号派对,等待窗口叫号,先来的人取到较小的号,也是最先被通知办理业务的,有些客人如果等得不耐烦,临时走掉了,也不会影响后边的人办理业务。
  • 这里的分布式锁实现就是一个公平锁的实现方案了,因为最先过来的获取锁的进程,也较先获得锁。
    分布式学习之分布式锁原理与实现
    分布式学习之分布式锁原理与实现

参考文章

一文了解分布式锁
网易云课堂《Java高级开发工程师》
redis分布式锁,面试官请随便问,我都会

结语

本人所有博客仅用于学习记录,不做任何商业用途,如涉及侵权,还请联系删除,感谢阅读,欢迎留言,一起进步~