分布式锁应该具有的特性(Safety & Liveness)

我们将从三个特性的角度出发来设计RedLock模型:

  1. 安全性(Safety):在任意时刻,只有一个客户端可以获得锁(排他性)。
  2. 避免死锁:客户端最终一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达。
  3. 容错性:只要Redsi集群中的大部分节点存活,client就可以进行加锁解锁操作。

通过Redis为某个资源加锁的最简单方式就是在一个Redis实例中使用过期特性(expire)创建一个key, 如果获得锁的客户端没有释放锁,那么在一定时间内这个Key将会自动删除,避免死锁。
这种做法在表面上看起来可行,但分布式锁作为架构中的一个组件,为了避免Redis宕机引起锁服务不可用, 我们需要为Redis实例(master)增加热备(slave),如果master不可用则将slave提升为master。
这种主从的配置方式存在一定的安全风险,由于Redis的主从复制是异步进行的, 可能会发生多个客户端同时持有一个锁的现象。

此类场景是非常典型的竞态模型:

  1. Client A 获得在master节点获得了锁
  2. 在master将key备份到slave节点之前,master宕机
  3. slave 被提升为master
  4. Client B 在新的master节点处获得了锁,Client A也持有这个锁。

在单redis实例中实现锁是分布式锁的基础,在解决前文提到的单实例的不足之前,我们先了解如何在单点中正确的实现锁。
如果你的应用可以容忍偶尔发生竞态问题,那么单实例锁就足够了。

我们通过以下命令对资源加锁
SET resource_name my_random_value NX PX 30000
SET NX 命令只会在Key不存在的时给key赋值,PX 命令通知redis保存这个key 30000ms。
my_random_value必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性。

通过下面的脚本为申请成功的锁解锁:
if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])
else
return 0
end

如果key对应的Value一致,则删除这个key。

通过这个方式释放锁是为了避免client释放了其他client申请的锁。

例如:
  • Client A 获得了一个锁,当尝试释放锁的请求发送给Redis时被阻塞,没有及时到达Redis。
    锁定时间超时,Redis认为锁的租约到期,释放了这个锁。
  • client B 重新申请到了这个锁
  • client A的解锁请求到达,将Client B锁定的key解锁
  • Client C 也获得了锁
  • Client B client C 同时持有锁。

通过执行上面脚本的方式释放锁,Client的解锁操作只会解锁自己曾经加锁的资源。
官方推荐通从 /dev/urandom/中取20个byte作为随机数或者采用更加简单的方式, 例如使用RC4加密算法在/dev/urandom中得到一个种子(Seed),然后生成一个伪随机流。
也可以用更简单的使用时间戳+客户端编号的方式生成随机数,
这种方式的安全性较差一些,但是对于绝大多数的场景来说也已经足够安全了。

PX 操作后面的参数代表的是这key的存活时间,称作锁过期时间。

当资源被锁定超过这个时间,锁将自动释放。
获得锁的客户端如果没有在这个时间窗口内完成操作,就可能会有其他客户端获得锁,引起争用问题。
通过上面的两个操作,我们可以完成获得锁和释放锁操作。如果这个系统不宕机,那么单点的锁服务已经足够安全,接下来我们开始把场景扩展到分布式系统。