分布式学习之分布式锁原理与实现
- 分布式锁的应用场景
- 分布式锁的原理、实现方式
- 基于zookeeper的分布式锁实现
1 分布式锁的应用场景
业务:创建具有业务含义的订单编号。
在单机情况下,我们的实现:订单服务类 -> 订单编号生成类,一个订单服务对象对应一个订单编号生成对象。
问题:如果同时有很多人下单,订单号能否做到唯一?
此时多个线程访问,如果一个线程一个订单编号生产对象,则订单号肯定不唯一,需要将订单编号生成对象改为静态共享资源,在此基础上再进行加锁。
在高并发、大数据量的情况下,单台服务器已经无法撑起并发量,为了保证服务调用的效率,我们会进行集群部署。
此时,基于jvm的锁都无法保证生成的订单号是唯一的。
1.1 错误的加锁姿势
在单个服务器中对订单服务类进行加锁
以上方案虽然对订单服务类加上了锁,但由于各个tomcat中使用的是各自的锁,所以订单号做不到唯一。
1.2 正确的加锁姿势
所有想要调用订单编号服务的service都必须争抢一把分布式锁,只有获得锁的service,才能接着调用订单编号服务。
问题:为什么非要多此一举,不直接给订单编号生成服务加锁呢?
- 为了避免单点故障
- 进行访问控制,只有通过认证,被允许访问的服务才能调用订单编号生成服务
分布式锁的用途:在分布式环境下,协同共享资源的使用,在单机环境下,没有必要使用。
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的分布式锁实现
- zookeeper简介
- zookeeper节点类型
- zookeeper典型应用场景
- 用zookeeper实现分布式锁逻辑一
- 改进后的实现数据结构和逻辑
3.1 zookeeper简介
- 分布式的、开源的分布式应用程序协调服务
特性:节点树数据结构,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据。
3.2 zookeeper节点类型
- 持久节点
- 持久顺序节点
- 临时节点
- 临时顺序节点
3.3 zookeeper典型应用场景
- 数据发布订阅(配置中心)——分布式指挥官
- 命名服务
- Master选举
- 集群管理
- 分布式队列
- 分布式锁
3.4 用zookeeper实现分布式锁逻辑一
特性:zookeeper中同父的子节点不可重名
- 加锁的逻辑
所有进程通过zk客户端连接zk,在zk的某个路径下边创建一个同名的临时节点,临时节点创建成功,即认为加锁成功,zookeeper类似于一个文件系统,是树形数据结构,在同一个目录下,不允许出现两个同名的子节点,所以有且仅有一个进程能够最终创建该临时节点。具体实现的时候,可以通过CountDownLatch的await()
(Java)来进行阻塞等待,因为如果使用park/unpark机制需要知道具体阻塞的进程。
- 解锁的逻辑
当某一进程获得锁,执行完业务后,开始释放锁,即删除该临时节点,由于临时节点过期自动删除的特性,所以就算该进程没有主动释放锁,也不会出现其他进程一直阻塞等待,造成死锁的情况。其他没有成功创建临时节点的进程,则在该节点路径**册watcher,一旦节点发生变动(被删除),则通知(唤醒)该进程,进程得到消息后,取消watcher,重新创建临时节点。
该分布式锁实现方案(非公平锁,有些进程很早就过来等待锁了,却要跟所有进程一起争抢锁的占有权)在大规模并发的情况下,会出现惊群效应。就好比,我们在给池塘里的鱼喂面包,一小块面包屑丢下去,会有成群结队的鱼簇拥过来,但是最后有且只有一条鱼能够吃到这个面包屑,从结果导向来看,其它的鱼完全没有必要消耗体力游过来争抢。因此,我们释放锁的时候,不需要通知所有阻塞等待的节点。
惊群效应的危害
- 巨大的服务器性能损耗
- 网络冲击
- 可能造成宕机
3.5 改进后的实现数据结构和逻辑
- 所有争抢锁的进程,不再是创建一个同名的临时节点了,而是创建顺序临时节点,意味着所有节点的名字不一样,且按照从小到大的顺序分布在zookeeper中lock路径下。
- 不会出现惊群效应,因为进程只需要监听前一个顺序临时节点的状态,当前一个节点退出等待或者被删除后,将通知后面的进程加锁,这就跟银行办理业务一样,所有的人都需要取号派对,等待窗口叫号,先来的人取到较小的号,也是最先被通知办理业务的,有些客人如果等得不耐烦,临时走掉了,也不会影响后边的人办理业务。
- 这里的分布式锁实现就是一个公平锁的实现方案了,因为最先过来的获取锁的进程,也较先获得锁。
参考文章
一文了解分布式锁
网易云课堂《Java高级开发工程师》
redis分布式锁,面试官请随便问,我都会
结语
本人所有博客仅用于学习记录,不做任何商业用途,如涉及侵权,还请联系删除,感谢阅读,欢迎留言,一起进步~