高可靠的分布式锁

136 阅读4分钟

单机系统下,使用synchronized或者ReentrantLock等这些常规的加锁方式来实现。 然而对于一个分布式集群的系统而言,单纯的本地锁已经无法解决问题,所以就需要用到分布式锁了,通常我们都会引入三方组件或者服务来解决这个问题,比如数据库、Redis、Zookeeper等。

需要保证:互斥性,可重入,不死锁(锁超时)的特征。

1 数据库实现

乐观锁(版本号)和悲观锁(for update,不推荐)

2 redis

通过setnx实现

image.png

3 Zookeeper

创建临时顺序节点来实现的

image.png

  1. 当需要对资源进行加锁时,实际上就是在父节点之下创建一个临时顺序节点。
  2. 客户端A来对资源加锁,首先判断当前创建的节点是否为最小节点,如果是,那么加锁成功,后续加锁线程阻塞等待
  3. 此时,客户端B也来尝试加锁,由于客户端A已经加锁成功,所以客户端B发现自己的节点并不是最小节点,就会去取到上一个节点,并且对上一节点注册监听
  4. 当客户端A操作完成,释放锁的操作就是删除这个节点,这样就可以触发监听事件,客户端B就会得到通知,同样,客户端B判断自己是否为最小节点,如果是,那么则加锁成功

常用redis来实现,面对两个问题。 1 锁超时 线程1没实现完就被删除了,然后被的线程2获取锁成功。 2 锁的误删除 基于1的问题,线程1执行完后删除了线程2的锁。

解决方案: 对于问题1:

  • 估业务执行时间,锁的时间长于该时间
  • 自动续租,通过别的线程来为这个线程续时间 对于问题2: set value为线程id,然后删除的时候需要进行判断

RedLock方案

如果在master上加锁成功,此时还未同步到slave,master挂掉了,在新的还不存在这个锁,就会出现问题。 RedLock的理念下需要至少2个Master节点,多个Master节点之间完全互相独立,彼此之间不存在主从同步和数据复制。 主要步骤如下:

  1. 获取当前Unix时间
  2. 按照顺序依次尝试从多个节点锁,如果获取锁的时间小于超时时间,并且超过半数的节点获取成功,那么加锁成功。这样做的目的就是为了避免某些节点已经宕机的情况下,客户端还在一直等待响应结果。举个例子,假设现在有5个节点,过期时间=100ms,第一个节点获取锁花费10ms,第二个节点花费20ms,第三个节点花费30ms,那么最后锁的过期时间就是100-(10+20+30),这样就是加锁成功,反之如果最后时间<0,那么加锁失败
  3. 如果加锁失败,那么要释放所有节点上的锁

Redission的方案

segmentfault.com/a/119000002… 从性能以及资源上讲,肯定很差。 需要考虑各种GC等问题的影响。

加锁、可重入 加锁和解锁都是通过lua脚本去实现的,这样做的好处是为了兼容老版本的redis同时保证原子性。

主要的加锁逻辑也比较容易看懂,如果key不存在,通过hash的方式保存,同时设置过期时间,反之如果存在就是+1。

watchdog 也叫做看门狗,也就是解决了锁超时导致的问题,实际上就是一个后台线程,默认每隔10秒自动延长锁的过期时间。

默认的时间就是internalLockLeaseTime / 3internalLockLeaseTime默认为30秒。

首先,如果对于并发不高并且比较简单的场景,通过数据库乐观锁或者唯一主键的形式就能解决大部分的问题。

然后,对于Redis实现的分布式锁来说性能高,自己去实现的话比较麻烦,要解决锁续租、lua脚本、可重入等一系列复杂的问题。

对于单机模式而言,存在单点问题。

对于主从架构或者哨兵模式,故障转移会发生锁丢失的问题,因此产生了红锁,但是红锁的问题也比较多,并不推荐使用,推荐的使用方式是用Redission。

但是,不管选择哪种方式,本身对于Redis来说不是强一致性的,某些极端场景下还是可能会存在问题。

对于Zookeeper的实现方式而言,本身就是保证数据一致性的,可靠性更高,所以不存在Redis的各种故障转移带来的问题,自己实现也比较简单,但是性能相比Redis稍差。

不过,实际中我们当然是有啥用啥,老板说用什么就用什么,我才不管那么多。