普通实现

下面的加锁操作直接使用一条原子命令即可,而解锁操作需要用到Lua脚本保证原子性,该实现只适用于在单节点上操作。

加锁

加锁可以使用Redis提供的一条原子命令完成:SET key value NX PX 30000

这里对其中的一些参数做一些解释:

  • key:我们使用key来当锁,因为key是唯一的。
  • value:为保证可靠性,加锁和解锁要是同一个客户端,客户端自己不能把别人加的锁给解了。所以这里value可用于标识客户端,解锁时需要进行比较。
  • NX:当key不存在时才进行set操作,若key已经存在,则不做任何操作。这个参数保证了只有一个人能拿到锁。
  • PX:这里其实可以传入EXPX,主要目的是设置一个过期时间,锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。

解锁

为确保原子性,这里使用Lua脚本实现Redis分布式锁的解锁操作:

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

实现的关键在于一定要先比较value是否相等,这也是上面加锁时提到的客户端自己不能把别人加的锁给解了,如果是同一个客户端那么就直接将key删除即可。

存在的问题

事实上,上面的分布式锁实现在Redis单机部署的场景下工作是没问题的。但是如果Redis有多个节点的话,加锁就只能作用在一个Redis节点上,即使使用了哨兵或者集群方案保证高可用,如果master节点由于某些原因发生了主从切换,依然会出现锁丢失的情况:

  1. 在Redis的master节点上获取到锁
  2. 这个锁的key还没来得及同步到slave节点
  3. master故障,发生故障转移,slave节点升级为master节点
  4. 导致锁丢失

Redlock实现

由于上述的分布式锁只适用于单机环境,Redis作者基于分布式环境提出了一种更高级的实现方式:Redlock。

在Redis的分布式环境中,我们假设有N个Redis master节点,这些节点完全互相独立,不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁,我们确保将在每个实例上使用此方法获取和释放锁。这里假设有5个Redis master节点,并且运行在5台不同的服务器上以保证他们不会同时都宕掉。以下为加锁操作:

  1. 首先获取当前的本地时间
  2. 使用相同的key和value依次尝试在5个实例上申请锁。在获得锁的过程中,为每一个锁操作设置一个快速失败时间(如果想要获得一个10秒的锁,那么每一个锁操作的失败时间设为5-50ms)。这样可以避免客户端与一个已经故障的master节点通信占用太长时间,通过快速失败的方式尽快与集群中的其他节点完成锁操作。
  3. 客户端计算出与master获得锁操作过程中消耗的时间(即当前时间减去第一步记录的时间),当且仅当客户端获得锁的过程中消耗的时间小于锁的存活时间,并且在一半以上的master节点中都获得锁,才认为client成功的获得了锁。
  4. 如果已经获得了锁,客户端执行任务的有效时间是锁的存活时间减去获得锁的过程中所消耗的时间。
  5. 如果客户端获得锁的数量不足一半以上,或获得锁的时间超时,那么认为获得锁失败,客户端应尽快地释放(部分)已经成功取到的锁,这样其他的客户端就不必非得等到锁过完有效时间才能取到。

这个算法的核心思想其实在于只可能有一个客户端能获取到大部分master节点中的锁,也就避免了多个客户端都能获取到锁的情况。对于释放锁来说,过程就相对简单一些了:向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁。

参考资料