在许多环境中,分布式锁是一种非常有用的原语,其中不同的进程必须以互斥的方式与共享资源一起运行。
有许多库和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都使用不同的方法,并且许多库使用简单的方法,与稍微复杂的方法相比可以实现更低的保证设计。
本文试图提供一种更典型的算法来使用Redis实现分布式锁。 我们提出了一种称为Redlock的算法,它实现了一种我们认为比vanilla单实例方法更安全的DLM。 我们希望社区将对其进行分析,提供反馈,并将其作为实施或更复杂或替代设计的起点。
实现
在描述算法之前,这里有几个已经可用的实现的链接,可用于参考。
1 | Redlock-rb(Ruby实现)。 还有一个Redlock-rb的分支,它可以添加一个宝石,方便分发,也许更多。 |
安全和存活保障
我们将仅使用三个属性对我们的设计进行建模,从我们的角度来看,这些属性是以有效方式使用分布式锁所需的最低保证。
- 安全属性:相互排斥。 在任何给定时刻,只有一个客户端可以持有锁。
- 活力属性A:无死锁。 最终,即使锁定资源的客户端崩溃或被分区,也始终可以获取锁。
- 活力属性B:容错。 只要大多数Redis节点启动,客户端就能够获取和释放锁。
为什么基于故障转移的实现还不够
为了理解我们想要改进的内容,让我们分析大多数基于Redis的分布式锁库的当前状态。
使用Redis锁定资源的最简单方法是在实例中创建key。 key通常使用Redis expires功能在有限的生存时间内创建,因此最终它将被释放(我们列表中的属性2)。 当客户端需要释放资源时,它会删除key。
从表面上看,这很有效,但存在一个问题:这是我们架构中的单点故障。 如果Redis主机出现故障会怎样? 好吧,让我们添加一个奴隶! 如果主服务器不可用,请使用它。 遗憾的是,这不可行。 通过这样做,我们无法实现互斥的安全属性,因为Redis复制是异步的。
这个模型有明显的竞争条件:
- 客户端A获取主服务器中的锁。
- 在写入key之前,主机崩溃并发送到从机。
- 奴隶被提升为主人。
- 客户端B获取对已经拥有锁的相同资源的锁。 安全违规!
有时在特殊情况下,例如在故障期间,多个客户端可以同时保持锁定,这是完全正常的。 如果是这种情况,您可以使用基于复制的解决方案。 否则,我们建议实施本文档中描述的解决方案。
使用单个实例正确实现
在尝试克服上述单实例设置的限制之前,让我们在这个简单的情况下检查如何正确地进行操作,因为这实际上是一个可行的解决方案,在不时可以接受竞争条件的应用程序中,并且因为锁定到 单个实例是我们将用于此处描述的分布式算法的基础。
要获得锁,可以采取以下措施:
1 | SET resource_name my_random_value NX PX 30000 |
该命令仅在key尚不存在时才设置key(NX选项),过期时间为30000毫秒(PX选项)。 值“myrandomvalue”。 此值必须在所有客户端和所有锁定请求中都是唯一的。
基本上使用随机值是为了以安全的方式释放锁,使用一个告诉Redis的脚本:仅当key存在且存储在key上的值恰好是我期望的值时才删除key。 这是通过以下Lua脚本完成的:
1 | if redis.call("get",KEYS[1]) == ARGV[1] then |
这很重要,以避免删除由另一个客户端创建的锁。 例如,客户端可能获得锁定,在某些操作中被阻止的时间超过锁定有效时间(key将到期的时间),并且稍后移除已经由某个其他客户端获取的锁定。 仅使用DEL是不安全的,因为客户端可能会删除另一个客户端的锁定。 使用上面的脚本而不是每个锁都使用随机字符串“签名”,因此只有在客户端尝试删除锁时,锁才会被删除。