[Re:从零开始的分布式] 0.x——Reids实现分布式锁

上节提到了,分布式锁通常应满足如下要求,互斥性、高可用、高效率、可重入、锁失效这五个基本原则。由于Redis自身“快”的特点,所以高效率可以看作满足。

下文在单机情况下与多机情况下,对利用Redis实现分布式锁做出了阐述。

单机Redis分布式锁

由于Redis本身的单线程特性,所以可以采用设置一个值的方式进行分布式锁。通常采用SETNX(set if not exist),进行设置,对资源使用完毕后,使用DEL进行删除。可以根据SETNX的返回值得到,是否成功获得锁,这一定程度上实现了互斥性,代码如下:

SETNX lock 1
...do something
DEL lock

但这时可能会存在一个问题,如果当前机器在do something的时候宕机,锁不能得到释放,导致系统死锁,所以这里需要锁失效机制,通常的锁失效,是通过设置过期时间完成的,即,

SETNX lock 1
EXPIRE lock 5
...do something
DEL lock

注意,常见面试题出现。Redis这样设置分布式锁是正确的吗?答案显然是否定的,因为 SETNX 与 EXPIRE 是两条指令,如果中间发生了宕机,同样会导致死锁。所以这里需要一条原子性的指令完成,SET指令提供了expire参数,这里利用这个参数进行完成。

SET lock 1 ex 5 nx
...do something
DEL lock

但是这样还存在问题,考虑如下情景:

1. A设置了一个锁,但是工作还没完成,锁超时释放了。

2. 此时B申请锁,由于超时释放,申请到了锁。

这导致了目前可以有A、B同时访问资源。

所以,考虑如下情形,应当在锁快失效时,更新失效时间。

但这还存在问题,存在全局时钟依赖的问题,比如Redis和客户端的时钟不一致,仍会导致问题。但目前依赖于时钟的具有自动释放锁的分布式锁都没办解决这个问题,不过由于发生概率很小,所以可以不做考虑。

关于可重入锁实现,可以通过利用Java的ThreadLocal工具,在客户端进行操作。lock时对“入锁值”+1;unlock时对"入锁值"-1,如果"入锁值"等于0,则从redis中释放锁。

多机Redis分布式锁

多机Redis分布式锁,Redis采用了RedLock对其进行实现,并在redisson中进行了实现。RedLock主要步骤如下:

  1. 取得当前时间
  2. 使用上文提到的方法依次获取N个节点的Redis锁。
  3. 如果获取到的锁的数量大于 (N/2+1)个,且获取的时间小于锁的有效时间(lock validity time)就认为获取到了一个有效的锁。锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
  4. 如果获取锁的数量小于 (N/2+1),或者在锁的有效时间(lock validity time)内没有获取到足够的说,就认为获取锁失败。这个时候需要向所有节点发送释放锁的消息。

但Martin对这提出了质疑:

  1. 如果 Client 1 在持有锁的时候,发生了一次很长时间的 FGC 超过了锁的过期时间。锁就被释放了。
  2. 这个时候 Client 2 又获得了一把锁,提交数据。
  3. 这个时候 Client 1 从 FGC 中苏醒过来了,又一次提交数据。

此时,数据发生了错误。

 

并且,Martin同时指出,RedLock是一个严格依赖于全局时钟的系统。

  1. Client 1 从 A、B、D、E五个节点中,获取了 A、B、C三个节点获取到锁,我们认为它持有了锁。
  2. 这个时候,由于 B 的系统时间比别的系统走得快,B就会先于其他两个节点优先释放锁。
  3. Clinet 2 可以从 B、D、E三个节点获取到锁。在整个分布式系统就造成 两个 Client 同时持有锁了。

 

所以,可以看出,Redis适用于实现高可用的分布式锁。但是对于需要在保证可用性的同时,数据正确性也需要严格保证时,并不适用于实现分布式锁。

posted @ 2019-04-20 17:09  妖妖灵!  阅读(231)  评论(0编辑  收藏  举报