zookeeper分布式锁

作者 : 开心源码 本文共3073个字,预计阅读时间需要8分钟 发布时间: 2022-05-12 共64人阅读

1 分布式锁的概念与数据最终不一致性的场景

随着互联网技术的不断发展,数据量的不断添加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应使用在更多的场景,而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,就需要使用到分布式锁。

分布式一致性问题

假设某商城有一个商品库存剩10个,使用户A想要买6个,使用户B想要买5个,在理想状态下,使用户A先买走了6了,库存减少6个还剩4个,此时使用户B应该无法购买5个,给出数量不足的提醒;而在真实情况下,使用户A和B同时获取到商品剩10个,A买走6个,在A升级库存之前,B又买走了5个,此时B升级库存,商品还剩5个,这就是典型的电商“秒杀”活动。

不做解决将会出现各种不可预知的后果。那么在这种高并发多线程的情况下,处理问题最有效最普遍的方法就是给共享资源或者对共享资源的操作加一把锁,来保证对资源的访问互斥。
在Java JDK已经为我们提供了这样的锁,利使用ReentrantLcok或者者synchronized,就可达到资源互斥访问的目的。
但是在分布式系统中,因为分布式系统的分布性,即多线程和多进程并且分布在不同机器中,这两种锁将失去原有锁的效果,需要我们自己实现分布式锁——分布式锁。

分布式锁需要具有哪些条件

  • 获取锁和释放锁的性能要好
  • 判断能否取得锁必需是原子性的操作,否则可能导致多个请求都获取到锁
  • 网络中断或者宕机无法释放锁时,锁必需被清理,不然会发生死锁
  • 可重入一个线程中屡次获取同一把锁,比方一个线程在执行一个带锁的方法,该方法中又调使用了另一个需要相同锁的方法,则该线程可以直接执行调使用的方法,而无需重新取得锁;
  • 阻塞锁和非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。

分布式锁实现方式

一、数据库锁

  • 基于MySQL锁表
    完全依靠数据库唯一索引来实现,当想要取得锁时,即向数据库中插入一条记录,释放锁时就删除这条记录
    这种方式存在以下问题:
    • 锁没有失效时间,解锁失败会导致死锁,其余线程无法再获取到锁,由于唯一索引insert都会返回失败
    • 只能是非阻塞锁,insert失败直接就报错了,无法进入队列进行重试
    • 不可重入,同一线程在没有释放锁之前无法再获取到锁
  • 采使用乐观锁
    添加版本号,根据版本号来判断升级之前有没有其余线程升级过,假如被升级过,则获取锁失败

二、缓存锁
这里主要是几种基于redis的

  • 基于setnxexpire
    基于setnx(set if not exist)的特点,当缓存里key不存在时,才会去set,否则直接返回false
    假如返回true则获取到锁,否则获取锁失败,为了防止死锁,我们再使用expire命令对这个key设置一个超时时间来避免。
    但是这里看似完美,实则有缺陷,当我们setnx成功后,线程发生异常中断,expire还没来的及设置,那么就会产生死锁。

处理上述问题有两种方案

  • 采使用redis2.6.12版本以后的set,它提供了一系列选项
    EX seconds – 设置键key的过期时间,单位时秒
    PX milliseconds – 设置键key的过期时间,单位时毫秒
    NX – 只有键key不存在的时候才会设置key的值
    XX – 只有键key存在的时候才会设置key的值

  • 第二种采使用setnx(),get(),getset()
    (1) 线程Asetnx,值为超时的时间戳(t1),假如返回true,取得锁。
    (2) 线程B使用get 命令获取t1,与当前时间戳比较,判断能否超时,没超时false,假如已超时执行步骤3
    (3) 计算新的超时时间t2,用getset命令返回t3(这个值可能其余线程已经修改过),假如t1==t3,取得锁,假如t1!=t3说明锁被其余线程获取了
    (4) 获取锁后,解决完业务逻辑,再去判断锁能否超时,假如没超时删除锁,假如已超时,不使用解决(防止删除其余线程的锁)

  • RedLock算法

redlock算法是redis作者推荐的一种分布式锁实现方式
(1) 获取当前时间;
(2) 尝试从5个相互独立redis用户端获取锁
(3) 计算获取所有锁消耗的时间,当且仅当用户端从多数节点获取锁,并且获取锁的时间小于锁的有效时间,认为取得锁
(4) 重新计算有效期时间,原有效时间减去获取锁消耗的时间
(5) 删除所有实例的锁
redlock算法相对于单节点redis锁可靠性要更高,但是实现起来条件也较为苛刻
(1) 必需部署5个节点才能让Redlock的可靠性更强
(2) 需要请求5个节点才能获取到锁,通过Future的方式,先并发向5个节点请求,再一起取得响应结果,能缩短响应时间,不过还是比单节点redis锁要耗费更多时间
而后因为必需获取到5个节点中的3个以上,所以可能出现获取锁冲突,即大家都取得了1-2把锁,结果谁也不能获取到锁,这个问题,redis作者借鉴了raft算法的精髓,通过冲突后在随机时间开始,可以大大降低冲突时间,但是这问题并不能很好的避免,特别是在第一次获取锁的时候,所以获取锁的时间成本添加了
假如5个节点有2个宕机,此时锁的可使用性会极大降低,首先必需等待这两个宕机节点的结果超时才能返回,另外只有3个节点,用户端必需获取到这一律3个节点的锁才能拥有锁,难度也加大了
假如出现网络分区,那么可能出现用户端永远也无法获取锁的情况
介于这种情况,下面我们来看一种更可靠的分布式锁zookeeper锁

zookeeper分布式锁

zookeeper是一个为分布式应使用提供一致性服务的软件,它内部是一个分层的文件系统目录树结构,规定统一个目录下只能有一个唯一文件名

数据模型

  • 永久节点
    节点创立后,不会由于会话失效而消失
  • 临时节点
    与永久节点相反,假如用户端连接失效,则立即删除节点
  • 顺序节点
    与上述两个节点特性相似,假如指定创立这类节点时,zk会自动在节点名后加一个数字后缀,并且是有序的

监视器(watcher):

当创立一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向用户端发送且仅发送一条通知,由于watch只能被触发一次

根据zookeeper的这些特性来实现分布式锁

  1. 创立一个锁目录lock
  2. 希望取得锁的线程A就在lock目录下,创立临时顺序节点
  3. 获取锁目录下所有的子节点,而后获取比自己小的兄弟节点,假如不存在,则说明当前线程顺序号最小,取得锁
  4. 线程B获取所有节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)
  5. 线程A解决完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,取得锁。

小结

在分布式系统中,共享资源互斥访问问题非常普遍,而针对访问共享资源的互斥问题,常使用的处理方案就是用分布式锁,这里只详情了几种常使用的分布式锁,分布式锁的实现方式还有有很多种,根据业务选择合适的分布式锁
下面对上述几种锁进行一下比较:

数据库锁

优点:直接用数据库,用简单。
缺点:分布式系统大多数瓶颈都在数据库,用数据库锁会添加数据库负担。

缓存锁

优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常用,建议采使用缓存锁。
缺点:通过锁超时机制不是十分可靠,当线程取得锁后,解决时间过长导致锁超时,就失效了锁的作使用。

zookeeper锁

优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采使用zookeeper锁。
缺点:性能比不上缓存锁,由于要频繁的创立节点删除节点。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » zookeeper分布式锁

发表回复