分布式锁概述

分布式锁概述

一、需具备的条件

1、获取 / 释放锁性能要好。
2、原子性。
3、网络中断或宕机无法释放锁时,锁必须被清除,不然会发生死锁。
4、可重入一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。
5、阻塞锁 / 非阻塞锁,阻塞锁即没有获取到锁,则继续等待获取锁;非阻塞锁即没有获取到锁后,不继续等待,直接返回锁失败。

二、实现方式

2.1、数据库锁

一般很少使用数据库锁,性能不好并且容易产生死锁。

2.1.1、基于 MySQL 锁表

该实现方式完全依靠数据库唯一索引来实现,当想要获得锁时,即向数据库中插入一条记录,释放锁时就删除这条记录。这种方式存在以下几个问题:
1、锁没有失效时间,解锁失败会导致死锁,其他线程无法再获取到锁,因为唯一索引 insert 都会返回失败。
2、只能是非阻塞锁,insert 失败直接就报错了,无法进入队列进行重试。
3、不可重入,同一线程在没有释放锁之前无法再获取到锁。

2.1.2、 采用乐观锁增加版本号

根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。

2.2、缓存锁

2.2.1、基于 setnx、expire 两个命令来实现

基于 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、获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)。

2、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 锁。

2.3、zookeeper 分布式锁

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

数据模型:
永久节点:节点创建后,不会因为会话失效而消失。
临时节点:与永久节点相反,如果客户端连接失效,则立即删除节点。
顺序节点:与上述两个节点特性类似,如果指定创建这类节点时,zk 会自动在节点名后加一个数字后缀,并且是有序的。
监视器(watcher):
当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch 被触发时,ZooKeeper 将会向客户端发送且仅发送一条通知,因为 watch 只能被触发一次。

根据 zookeeper 的这些特性,我们来看看如何利用这些特性来实现分布式锁:
1、创建一个锁目录 lock。
2、希望获得锁的线程 A 就在 lock 目录下,创建临时顺序节点。
3、获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁。
4、线程 B 获取所有节点,判断自己不是最小节点,设置监听 (watcher) 比自己次小的节点(只关注比自己次小的节点是为了防止发生 “羊群效应”)。
5、线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是最小的节点,获得锁。

三、小结

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

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

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

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

评论

暂无

添加新评论