redis分布式锁安全性探讨

一、基于单个redis节点的分布式锁

步骤1:向redis发送命令,获取锁

SET resource_name my_random_value NX PX 30000

 

解释说明:

my_random_value:客户端生成的随机,要保证在足够长的时间内所有客户端生成的随机值是唯一的

NX:当key为resource_name的值不存在时,才能被成功插入(IF NOT EXISTS)。这一点保证了只有一个客户端能设置成功,换句话说只有一个客户端能拿到锁。

PX:表示过期时间为30s

 

步骤2:获取锁成功后才能访问共享资源

步骤3:释放锁

if redis.call("get",KEYS[1]) == ARGV[1] then

    return redis.call("del",KEYS[1])

else

    return 0

end

备注:KEYS[1]为resource_name,ARGV[1]:为my_random_value

 

二、重点问题分析

1、必须设置过期时间:防止客户端崩溃或者跟redis通信中断时,导致无法释放锁的问题出现

2、要保持操作的原子性

网络上出现很多介绍redis锁是用以下方式:

1、SETNX resource_name my_random_value

2、EXPIRE resource_name 30

 

虽然这两个命令跟前面set nx px的效果一样,但是它将设置key+设置过期时间的操作分开,不能保证获取锁操作的原子性。

  • 当客户端执行完setnx后崩溃,那么它就没有机会释放锁
  • 当客户端执行完setnx成功且expire了过期时间,但可能在它访问共享资源时锁过期了,这时可能已经有另外一个客户端也在访问同一个共享资源。

 

3、客户端生成的my_random_value随机值是唯一的,这样才能保证在释放锁时删除的key是自己生成的那一个(不会出现误删)

假如获取锁时SET的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:

 

  1. 客户端1获取锁成功。
  2. 客户端1在某个操作上阻塞了很长时间。
  3. 过期时间到了,锁自动释放了。
  4. 客户端2获取到了对应同一个资源的锁。
  5. 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。

 

之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。

 

4、释放锁的操作必须使用Lua脚本来实现(为了保证删除操作的原子性)

释放锁其实包含三步操作:'GET'、判断和'DEL',用Lua脚本来实现能保证这三步的原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,就有可能发生与前面第三个问题类似的执行序列:

 

  1. 客户端1获取锁成功。
  2. 客户端1访问共享资源。
  3. 客户端1为了释放锁,先执行'GET'操作获取随机字符串的值。
  4. 客户端1判断随机字符串的值,与预期的值相等。
  5. 客户端1由于某个原因阻塞住了很长时间。
  6. 过期时间到了,锁自动释放了。
  7. 客户端2获取到了对应同一个资源的锁。
  8. 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

 

实际上,在上述第三个问题和第四个问题的分析中,如果不是客户端阻塞住了,而是出现了大的网络延迟,也有可能导致类似的执行序列发生。

 

三、基于单redis节点无法解决的问题

#若redis没有配置高可用,当redis唯一节点宕机后,那么所有客户端就无法获得锁了,锁机制失效。因此为了提高redis的高可用,设置了主从节点。

也因此产生了另外一个新问题。

#问题描述:我们可以给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但由于Redis的主从复制(replication)是异步的(即主从复制存在一定的时间差,很容易在这个时间差里打擦边球),这可能导致在failover过程中丧失锁的安全性。

考虑下面的执行序列:

 

  1. 客户端1从Master获取了锁。
  2. Master宕机了,存储锁的key还没有来得及同步到Slave上。
  3. Slave升级为Master。
  4. 客户端2从新的Master获取到了对应同一个资源的锁。

 

于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。针对这个问题,antirez设计了Redlock算法

 

其它疑问

1、px time设置成多少合适呢?设置少了可能会导致客户端访问共享资源之前[锁过期],设置太长了呢,可能会导致客户端释放锁失败时,导致其它客户端的长时间等待。

 

2、可能因为长时间的阻塞(网络延迟),导致客户端获得的锁过期,而使得共享资源失去了保护,那么有没有什么方案来解决这些存在的疑问呢?

 

 

基于单个redis实现的分布式锁,存在无法解决的问题,一起回顾如下

1、为了提高单redis的可用性,给master挂了一个从slave节点,因为主从复制是异步的,会出现不同客户端同时获取锁的情况

  • 客户端1在master中获得了锁,
  • 在锁同步到slave之前,master宕机,还未来得及将锁同步到slave
  • slave升级为master
  • 客户端2在新的master中获取了锁

这样客户端1、客户端2就同时持有了同一个资源的锁

2、客户端1获取锁,因网络延时,客户端长时间阻塞,锁过期,这时客户端2获取锁,与此同时客户端1的网络恢复正常,这就导致两个客户端可同时访问共享资源。

备注:有两种情况会引发阻塞

  • 情况1:客户端跟redis通信的问题,
  • 情况2:客户端跟共享资源服务器交互延时

 

 

那么接下来我看看Redlock是否能解决上述两个问题

 

一、分布式锁Redlock

 

antirez因为上述问题提出了新的分布式锁的算法Redlock,它基于N个完全独立的Redis节点(通常情况下N可以设置成5)。

 

运行Redlock算法的客户端依次执行下面各个步骤,来完成获取锁的操作:

 

  1. 获取当前时间(毫秒数)。
  2. 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
  5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。

 

当然,上面描述的只是获取锁的过程,而释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。

 

二、Redlock存在的问题

 

由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了(解决了遗留问题1),但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。

根据上述提出的算法,当N个节点中有一个节点宕机,仍然存在锁的安全性问题。具体的影响跟redis的持久化程度有关

 

假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

 

  1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
  3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功。

 

这样,客户端1和客户端2同时获得了锁(针对同一资源)。

 

在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

 

关于Redlock还有一点细节值得拿出来分析一下:

 

在最后释放锁的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起释放锁的操作。也就是说,即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。

 

这是为什么呢?设想这样一种情况,客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。

 

三、其它问题

1、仍然存在开篇我们提到的第2个问题:客户端长时间阻塞,导致获得的锁释放,访问的共享资源不受保护的问题。

2、在Redlock的算法中,我们可以看到第3步,当获取锁耗时太多,留给客户端的访问共享资源的时间很短,这种情况若来不及操作,是不是要释放锁呢?且到底剩下多少时间才算短?这又是一个选择难题。

3、Redlock算法对时钟依赖性太强,若N个节点中的某个节点发生时间跳跃,也可能会引此而引发锁安全性问题。

 

原文:https://blog.csdn.net/hh1sdfsf56456/article/details/79474409

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值