Redis实现的分布式锁

按照Redis官方的说法,Redis实现的分布式锁需要确保下面三个属性:

  1. 安全性:互斥,在指定时间点,只能有一个客户端持有锁;
  2. 存活性A:不能有死锁。就是始终能获取到一个锁,即使客户端锁定的资源崩溃或者被分区;
  3. 存活性B:容错能力。只要大多数Redis节点还在运行,客户端就能获取和释放锁。

在单实例Redis的情况下实现分布式锁是比较简单的,直接通过SET一个客户端的唯一标识值,并给他设定超时时间即可。
但是在多实例的情况下,这种实现方案是有致命缺陷的。假如出现下面的情况:

  1. 客户端A在master获取锁
  2. 在master把该key同步到slave之前,master崩溃了
  3. slave晋升为master
  4. 客户端B在这个时候获取的相同的资源,但是在新的master中没有对应key

这种情况下相当于客户端AB都拿到了锁,违背了之前讲到的第一个属性:安全性。

下面是单实例Redis具体是怎么实现分布式锁的

单实例Redis

首先是获取锁

SET resource_name my_random_value NX PX 30000

NX代表只能在没有对应key的时候才能设置成功,PX代表无论怎样该key都会在30000毫秒后失效。
注意这里要确保my_random_value值的唯一性。

除了超时释放锁,客户端也会主动释放锁,主动释放锁通过lua脚本实现:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这样写的原因是确保客户端删的是自己的锁。

多实例Redis

Redis提供了一个多实例分布式锁的解决方案,Redis称这个算法为Redlock。
关于Redlock的实现有很多,涉及多种语言。

参考redis distributed lock

Redlock有多种语言实现,其中java实现叫做redisson(注意,在目前的redisson官网,已经不推荐Redlock,推荐他自身实现的RLock)

参考Redlock java implementation

这里假定有N个Redis的master节点,并且这些节点都是独立的,因此没有使用复制或者其他隐性的协作系统。
在这个例子里我们设定有5个Redis master节点,这些节点部署在不同的电脑或者虚拟机上。为了获取锁客户端要执行以下操作:

  1. 获取当前时间的毫秒值;
  2. 串行从五个节点中获取锁,使用相同的key值和value。当客户端从每个实例获取锁时,也要设置超时时间,这个时间肯定要比锁的自动释放时间要短。举个例子当自动释放时间是10秒时,那么超时时间可能在5-50毫秒这个范围。这样就避免了当Ridis挂了之后,客户端还长时间阻塞去获取连接。如果一个实例是不可用的,那么我们应该尽快跟下一个实例建立连接;
  3. 客户端计算获取锁这个过程一共花费了多少时间,(通过用当前时间减去步骤一中的时间)。只有客户端能从大多数实例中获取锁(至少三个),并且获取锁的总时间是比锁的有效时间短的,那么这个锁才能被叫做被获取;
  4. 如果锁被获取,那么有效时间应该考虑被设置为初始有效时间 减去 消耗的时间(步骤三计算得来)
  5. 如果客户端没有成功获取锁,他会尝试解锁所有实例(就算实例没有该客户端的锁信息)。

除了上面的定义和流程,该算法还需要其他优化:

  1. 客户端应该尽可能的同时获取多个节点的锁,就是并发获取;
  2. 如果一个客户端没能从节点中获取锁,那么下次重试的延后时间,应该是一个随机值,避免并发获取锁;
  3. 如果客户端获取锁失败,应该尽可能快的尝试解锁;
  4. 某个Redis宕机过后,应该设定延迟重启,延迟的时间长度基于当前激活的锁的有效时间。这样可以避免之前的客户端刚好三个锁,重启后当前节点的锁丢失,另外一个客户端又获取到三个锁的情况;
  5. 客户端的锁定目标流程如果是由多个小步骤组成的,那么可以把锁的有效时间设短,每个步骤对应一个锁,每次执行完都去申请重设锁的有效时间,当然这个重新申请的时机要在前一个锁还未失效之前,并且要注意设置一个重试的上限次数(类似实现:Redisson的看门狗机制)。

该算法其实并没有解决之前提出的主从节点复制,锁信息丢失的问题,而是通过没有从节点,多个主节点的方式来解决的。