分布式锁学习笔记

安全性与活跃性(liveness)保证

  • 安全性:互斥。在任意时刻,仅存在唯一客户拥有锁。
  • 活跃性A:死锁释放。最终肯定可以获取到一个锁,即便拥有着锁的客户崩溃宕机。
  • 活跃性B:容错。只要大多数节点正常,客户就能够获取和释放锁。

为什么基于故障转移(容错转移)的实现还不够?

基于REDIS的分布式锁库的简单实现方案:一个锁对应一个键的实例。利用REDIS的超时机制,最终肯定可以获取到锁。锁不存在,即未占用,存在即被占用。锁释放时,删除键即可。
但是这种方案存在问题:如果REDIS主节点崩溃,即便是在REDIS主从架构的集群,无法保证安全性(互斥性),因为REDIS的主从复制是异步的。
以下是该方案中一个触发竞争条件(race condition)的场景:

  1. 客户A从主节点获取锁
  2. 在主节点复制该锁数据到从节点前,主节点就崩溃了
  3. 从节点晋升为主节点
  4. 客户B从(新)主节点获取相同的锁(违反安全性)

单实例方案的正确实现

在竞争条件可接受的情况下,单实例方案是一个可行的方案。
获取锁

SET resource_name my_random_value NX PX 30000
注: 仅当键不存在时,该命令会创建键,30000ms超时。键值应该是全局(客户间)唯一的值,为了释放锁的权力仅有唯一且就是创建锁的客户所有(除超时)。

解锁

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

分布式方案实现

实现方案其实就是参照分布式一致性算法的,与PAXOS非常类似的。

初始化:

  • 设存在N个(N为奇数)节点,每个节点互相独立,不存在任何协同。

过程:

  1. 获取当前时间(ms)
  2. 尝试按顺序地从N个节点获取锁,使用相同的键名和键值。
    2.1. 客户请求每个节点时也会有一个超时,而该超时的值也有要求。要求是与锁自动释放的超时相比,请求超时要足够小(大概小于千分之五)。这是为了防止客户持续来自已崩溃的节点的响应。
  3. 客户计算获取到锁所花费的时间(当前时间 - 第一步所获得的时间)。向每个节点获取锁的所花费的总时间 小于 锁的自动超时 才认为锁是成功地被获取了。
  4. 如果锁被获取了,则 锁的超时时间 = 初始超时时间 - 获取锁所花费的时间
  5. 如果获取锁失败,则客户需要向所有节点发送释放锁的请求(即便被认为是获取锁失败的节点)

其他:

  • 对每个节点获取锁和释放锁的过程,同单实例版本一致。

算法是安全的吗?

假设一个客户能够从大多数节点中获取锁。所有的节点将会包含一致存活时间的一个键。但是,在异步的环境下,每个键被设置的时间是不相同的,因此它们超时的时间也是不相同的。
假设客户在第一个节点设置键的时间点是T1(请求第一个服务器的那一刻),在最后一个节点设置成功的时间是T2(从最后一个节点接收到设置成功的响应的一刻),那么,我们可以保证第一个被设置的键的最低有效存活时间是MIN_VALIDITY = TTL - (T2 - T1) - CLOCK_DRIFT。 ?

理论的严谨性我们可以参考分布式共识算法PAXOS,在此不作详述。

活跃性参数(Liveness arguments)

系统的活跃性基于三个主要的特点:

  • 自动释放锁:锁最终可以重新被获取和锁定。
  • 事实上,获取锁的客户,在崩溃的时候,都会释放自身所获取的锁以使锁可以被重新获取,而无需等待自动释放的时间。
  • 当客户需要重新请求获取锁时(前面一次尝试失败),它需要等待一段比从多数节点获取锁锁需要的时间。

在遇到网络分裂(network partition)的情况时,我们需要付出TTL时长的可用惩罚。如果存在连续分区,则需要付出无限的惩罚。这个情况经常发生在,客户获取锁了以后,被网络分裂,导致无法释放锁。
基本来说,如果系统内存在无限个连续的网络分区,系统会在无限长的一段时间中不可用。

性能、宕机恢复、持久化和延迟启动

性能

用户使用REDIS作为锁服务器的原因在于,它能提供获取/释放锁的超低延迟。
为了达到这一点,请求N个REDIS服务器节点的策略应该使用多路复用(multiplexing)。

宕机恢复和持久化

设计到宕机恢复的问题,持久化是一个经常被考虑的补救措施。通过宕机的节点重启后,重新读取前面持久化了的原数据镜像。
但是如果这种措施,会非常消耗性能。

延迟启动

实际上,重启后的节点,是对宕机前已存在的锁无感知。对于新的获取锁的请求,是可以正常工作的。
为了避免持久化造成大量性能消耗,在确保锁安全性的同时,我们可以使用延迟启动。宕机重启的节点,保持不可用一段时间,该时长应该比最大TTL(锁的生存时间)大一点。
使用该方法,甚至无需任何持久化措施,也能够保证系统的安全性。
但是,这样会牺牲系统的可用性,因为节点宕机恢复是牺牲了TTL时长的可用性的。如果短期内大量(过半数)节点崩溃,则在TTL时间段内,会产生锁系统全局不可用的情况,因为超半数节点处于宕机或延迟启动的不可用状态。

拓展锁——让算法更可靠

当锁的生存时间接近殆尽时,所获取锁的客户端可以向所有节点请求延长锁的生存时间。
延长锁的算法与请求锁的算法实际上是很类似的。
但是,为了防止违反性质-活跃性A,防止某个客户死锁,延长锁生存时间的申请是有上限的,不能无限申请。

参考

  • https://redis.io/topics/distlock

你可能感兴趣的:(算法)