Redis分布式锁
在许多环境中,分布式锁是一种非常有用的原语,其中不同的进程必须以互斥的方式与共享资源一起运行。
有许多库和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都使用不同的方法,而且许多库使用的是一种简单的方法,与稍微复杂的设计相比,可以获得较低的保障。
此页面试图提供一种更典型的算法来使用Redis实现分布式锁,我们提出了一种称为Redlock的算法,它实现了一种我们认为比vanilla单实例方法更安全的DLM,我们希望社区将对其进行分析,提供反馈,并将其作为实施或更复杂或替代设计的起点。
实现
在描述算法之前,这里有几个已经可用的实现的链接,可用于参考。
- Redlock-rb(Ruby实现),还有一个Redlock-rb的分支,添加一个gem以便分发,或者更多。
- Redlock-py(Python实现)
- Aioredlock(Asyncio Python实现)
- Redlock-php (PHP implementation)
- PHPRedisMutex(进一步的PHP实现)
- cheprasov/php-redis-lock(用于锁的PHP库)
- Redsync.go(Go实施)
- Redisson(Java实现)
- Redis::DistLock(Perl实现)
- Redlock-cpp(C ++实现)
- Redlock-cs(C#/.NET实现)
- RedLock.net(C#/.NET实现),包括异步和锁扩展支持
- ScarletLock(带可配置数据存储区的C#.NET实现)
- node-redlock(NodeJS实现),包括对锁定扩展的支持
安全性和活性保障
我们将仅使用三个属性对我们的设计进行建模,从我们的角度来看,这些属性是以有效方式使用分布式锁所需的最低保障。
- 安全属性:相互排斥。在任何给定时刻,只有一个客户端可以持有锁。
- 活力属性A:无死锁。最终即使锁定资源的客户端崩溃或被分区,也始终可以获取锁定。
- 活力属性B:容错。只要大多数Redis节点启动,客户端就能够获取和释放锁。
为什么基于故障转移的实现还不够
为了理解我们想要改进的内容,让我们分析大多数基于Redis的分布式锁库的当前状态。
使用Redis锁定资源的最简单方法是在实例中创建密钥,密钥通常使用Redis过期功能在有限的生存时间内创建,因此最终它将被释放(我们列表中的属性2),当客户端需要释放资源时,它会删除密钥。
从表面上看,这很有效,但存在一个问题:这是我们架构中的单点故障,如果Redis主机出现故障会怎样?好吧,让我们添加一个从机!如果主服务器不可用,则使用它,遗憾的是,这不可行。通过这样做,我们无法实现互斥的安全属性,因为Redis主从复制是异步的。
这种模式存在明显的竞争条件:
- 客户端A获取主服务器中的锁
- 在对密钥的写入被传输到从服务器之前,主服务器崩溃了
- 从机被提升为主机
- 客户端B获得与A已经持有锁的相同资源的锁。安全违反!
有时在特殊情况下,例如在故障期间,多个客户端可以同时保持锁定,这是完全正常的,如果是这种情况,你可以使用基于主从复制的解决方案,否则,我们建议实施本文档中描述的解决方案。
使用单个实例正确实现
在尝试克服上述单实例设置的限制之前,让我们看看在这个简单的例子中如何正确地做到这一点,因为在时常可以接收竞争条件的应用中,这实际上是一个可行的解决方案,因为锁定到单个实例是我们将用于此处描述的分布式算法的基础。
要获得锁,可采取的方法如下:
SET resource_name my_random_value NX PX 30000
该命令仅在密钥尚不存在时才设置密钥(NX选项),到期时间为30000毫秒(PX选项),键的值设置为“myrandomvalue”,此值必须在所有客户端和所有锁定请求中都是唯一的,基本上使用随机值以便以安全的方式释放锁,使用一个告诉Redis的脚本:只有当密钥存在并且密钥中存储的值正是我期望的那个时才删除密钥,这是通过以下Lua脚本完成的:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end