Redis-Redlock-锁-Redis分布式锁

本文最后更新于:2020年11月24日 上午

信息

官方算法文章:https://redis.io/topics/distlock
redlock-py pypi: https://pypi.org/project/redlock-py/
redlock-py github: https://github.com/SPSCommerce/redlock-py

安全和可靠性保证

实现高效分布式锁的基础

  • 一致性
    互斥。不管任何时候,只有一个客户端能持有同一个锁
  • 分区可容忍性
    不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区
  • 可用性
    只要大多数Redis节点正常工作,客户端应该都能获取和释放锁

锁的基础

Redis中本身包含一些命令可以用来实现锁的功能,比如SETNX
思路:
如果 key 不存在,将 key 设置为 value
如果 key 已存在,则 SETNX 不做任何动作

  1. 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
  2. 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
  3. 客户端A执行代码完成,删除锁
  4. 客户端B在等待一段时间后在去请求设置key的值,设置成功
  5. 客户端B执行代码完成,删除锁
1
2
$redis->setNX($key, $value);
$redis->expire($key, $ttl);

过期时间

如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。
key 设置过期时间能够确保锁最终能够得到释放

请求与等待 的循环

程序不能因为发现资源被锁上了就直接停止
过于频繁的请求会让服务器压力变大
等待的时间应该为一个合理的随机数,因为如果多个客户端同时重试,那么可能会导致谁都无法拿到锁的情况出现

但是用来设置过期时间的 Expire命令 并不是原子性操作了, 所以需要设置事务来确保原子性

  1. 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
  2. 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
  3. 客户端A执行代码完成,删除锁
  4. 客户端B在等待一段时间后在去请求设置key的值,设置成功
  5. 客户端B执行代码完成,删除锁
1
$redis->set($key, $value, array('nx', 'ex' => $ttl));  //ex表示秒

关于键的值

值会被填入一个随机数,且必须在所有获取锁请求的客户端里保持唯一
键值是用来保证能安全地释放锁的,删除前需要做对比。单纯的用DEL指令有可能造成一个客户端删除了其他客户端的锁

例: 为什么要设置一个特殊的值

  1. A客户端拿到了锁,被某个操作阻塞了很长时间,过了超时时间后自动释放了这个锁
  2. B客户端来了,申请,拿到了这个锁
  3. A客户端操作结束,尝试删除这个其实已经被其他客户端拿到的锁

    如果此时没有设置随机数作校对,那么会删掉B的锁

Redlock算法

假设有NRedis master节点

N是个奇数
因为流程中有 过半数 这种判断,如果是偶数,那么将无法完成判断
节点越多,越能避免部分节点宕机造成的影响,但也越浪费资源

  1. 获取当前时间(单位是毫秒)

  2. 用 相同的key和随机值 在N个节点上逐一请求锁
    客户端在每个master上请求锁时,会有一个比总的 锁释放时间 小很多的 连接超时时间

    比如:如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围

    设置连接超时时间 可以防止一个客户端在某个宕掉的master节点上阻塞过长时间(避免总连接时间接近/超过锁释放时间

  3. 客户端计算第二步中获取锁所花的总时间

  4. 进行判断
    只有当客户端在过半数master节点上成功获取了锁,而且 总共消耗的时间 不超过 锁释放时间,这个锁才会被认为是获取成功

  • 如果锁获取成功了
    那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间
  • 如果锁获取失败了
    客户端会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁

客户端如果没有在多数节点获取到锁,一定要尽快在获取锁成功的节点上释放锁,这样就没必要等到key超时后才能重新获取这个锁(但是如果网络分区的情况发生而且客户端无法连接到Redis节点时,会损失等待key超时这段时间的系统可用性)

扩展锁

如果客户端做的工作都是由一些小的步骤组成,那么就有可能使用更小的默认锁有效时间,而且扩展这个算法来实现一个锁扩展机制
客户端如果在执行计算期间发现锁快要超时了,客户端可以给所有服务实例发送一个命令,让master端延长锁的时间
只要这个锁的 key 还存在而且值还等于客户端获取时的那个值。 客户端应当只有在 失效时间内无法延长锁时 再去重新获取锁(基本上这个和获取锁的算法是差不多的)

安全性的论证

可以观察不同场景下的情况来理解这个算法为什么是安全的。

假设客户端可以在大多数节点都获取到锁,这样所有的节点都会包含一个有相同存活时间的key。需要注意的是,这个key是在不同时间点设置的,所以这些key也会在不同的时间超时

假设最坏情况下:

  • 第一个key是在T1时间设置的(客户端连接到第一个服务器时的时间)
  • 最后一个key是在T2时间设置的(客户端收到最后一个服务器返回结果的时间)

T2时间开始,确认最早超时的key至少也会存在的时间为

1
MIN_VALIDITY = TTL - (T2-T1) - CLOCK_DRIFT
  • TTL: 锁超时时间
  • (T2-T1): 最晚获取到的锁的耗时
  • CLOCK_DRIFT: 是不同进程间时钟差异,这个是用来补偿前面的(T2-T1)

其他的key都会在这个时间点之后才会超时,所以我们可以确定这些key在这个时间点之前至少都是同时存在的

  • 无法抢占
    在过半数master节点的keyset了的时间段内,其他客户端无法抢占这个锁
    因为在 (N/2)+1master 端的 key 已经存在的情况下不可能再在(N/2)+1master端上获取锁成功,所以如果一个锁获取成功了,就不可能同时重新获取这个锁成功(不然就违反了分布式锁互斥原则)

  • 多个客户端同时尝试获取锁时不会都同时成功
    如果一个 客户端获取过半数节点锁的耗时 接近甚至超过 锁的最大有效时间,那么系统会认为这个锁是无效的同时会释放这些节点上的锁,所以仅仅需要考虑获取 过半数节点所的耗时 小于 锁有效时间 的情况,而在这种情况下,根据我们前面的证明,在MIN_VALIDITY时间内,没有客户端能重新获取锁成功


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!