Redis分布式锁实现理解

Redis上,可以通过对key值的独占来实现分布式锁,表面上看,Redis可以简单快捷通过set key这一独占的方式来实现,也有许多重复性轮子,但实际情况并非如此。
总得来说,Redis实现分布式锁,如何确保锁资源的安全&及时释放,是分布式锁的最关键因素。
如下逐层分析Redis实现分布式锁的一些过程,以及存在的问题和解决办法。

solution 1 :setnx

setnx命令设置key的方式实现独占锁

1,#并发线程抢占锁资源
setnx an_special_lock 1
2,#如果1抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  execute business_method()
  3,#释放锁
   del an_special_lock

存在的问题很明显:
从抢占锁,然后并发线程中当前的线程操作,到最后的释放锁,并不是一个原子性操作,
如果最后的锁没有被成功释放(del an_special_lock),也即2~3之间发生了异常,就会造成其他线程永远无法重新获取锁

solution 2:setnx + expire key

为了避免solution 1中这种情况的出现,需要对锁资源加一个过期时间,比如是10秒钟,一旦从占锁到释放锁的过程发生异常,可以保证过期之后,锁资源的自动释放

1,#并发线程抢占锁资源
setnx an_special_lock 1
2,#设置锁的过期时间
expire an_special_lock 10
3,#如果1抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  execute business_method()
  4,#释放锁
   del an_special_lock

通过设置过期时间(expire an_special_lock 10),避免了占锁到释放锁的过程发生异常而导致锁无法释放的问题,
但是仍旧存在问题:
在并发线程抢占锁成功到设置锁的过期时间之间发生了异常,也即这里的1~2之间发生了异常,锁资源仍旧无法释放
solution 2虽然解决了solution 1中锁资源无法释放的问题,但与此同时,又引入了一个非原子操作,同样无法保证set key到expire key的以原子的方式执行
因此目前问题集中在:如何使得设置一个锁&&设置锁超时时间,也即这里的1~2操作,保证以原子的方式执行?

solution 3 : set key value ex 10 nx

Redis 2.8之后加入了一个set key && expire key的原子操作:set an_special_lock 1 ex 10 nx

1,#并发线程抢占锁资源,原子操作
set an_special_lock 1 ex 10 nx
2,#如果1抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  business_method()
  3,#释放锁
  del an_special_lock

目前,加锁&&设置锁超时,成为一个原子操作,可以解决当前线程异常之后,锁可以得到释放的问题。

但是仍旧存在问题:
如果在锁超时之后,比如10秒之后,execute_business_method()仍旧没有执行完成,此时锁因过期而被动释放,其他线程仍旧可以获取an_special_lock的锁,并发线程对独占资源的访问仍无法保证。

solution 4: 业务代码加强

到目前为止,solution 3 仍旧无法完美解决并发线程访问独占资源的问题。
笔者能够想到解决上述问题的办法就是:
设置business_method()执行超时时间,如果应用程序中在锁超时的之后仍无法执行完成,则主动回滚(放弃当前线程的执行),然后主动释放锁,而不是等待锁的被动释放(超过expire时间释放)
如果无法确保business_method()在锁过期放之前得到成功执行或者回滚,则分布式锁仍是不安全的。

1,#并发线程抢占锁资源,原子操作
set an_special_lock 1 ex 10 n
2,#如果抢占到当前锁,并发线程中的当前线程执行
if(成功获取锁)
  business_method()#在应用层面控制,业务逻辑操作在Redis锁超时之前,主动回滚
  3,#释放锁
  del an_special_lock

solution 5 RedLock: 解决单点Redis故障

截止目前,(假如)可以认为solution 4解决“占锁”&&“安全释放锁”的问题,仍旧无法保证“锁资源的主动释放”:
Redis往往通过Sentinel或者集群保证高可用,即便是有了Sentinel或者集群,但是面对Redis的当前节点的故障时,仍旧无法保证并发线程对锁资源的真正独占。
具体说就是,当前线程获取了锁,但是当前Redis节点尚未将锁同步至从节点,此时因为单节点的Cash造成锁的“被动释放”,应用程序的其它线程(因故障转移)在从节点仍旧可以占用实际上并未释放的锁。
Redlock需要多个Redis节点,RedLock加锁时,通过多数节点的方式,解决了Redis节点故障转移情况下,因为数据不一致造成的锁失效问题。
其实现原理,简单地说就是,在加锁过程中,如果实现了多数节点加锁成功(非集群的Redis节点),则加锁成功,解决了单节点故障,发生故障转移之后数据不一致造成的锁失效。
而释放锁的时候,仅需要向所有节点执行del操作。

Redlock需要多个Redis节点,由于从一台Redis实例转为多台Redis实例,Redlock实现的分布式锁,虽然更安全了,但是必然伴随着效率的下降。

至此,从solution 1-->solution 2-->solution 3--solution 4-->solution 5,依次解决个前一步的问题,但仍旧是一个非完美的分布式锁实现。

以下通过一个简单的测试来验证Redlock的效果。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/18e09d5bef4c2a6e0232dbe24bcb8e93.html