Redlock算法实现Redis分布式锁
为什么基于故障转移的实现还不够
使用 Redis 锁定资源的最简单方法是在实例中创建密钥。密钥通常是在有限的生存时间内创建的,使用 Redis 过期功能,以便最终它被释放(我们列表中的属性 2)。当客户端需要释放资源时,它会删除密钥。
从表面上看,这很好用,但有一个问题:这是我们架构中的单点故障。如果 Redis 主节点出现故障会怎样? 好吧,让我们添加一个副本!如果主站不可用,请使用它。不幸的是,这是不可行的。这样一来,我们就无法实现互斥的安全属性,因为 Redis 复制是异步的。
此模型存在竞争条件:
- 客户端 A 获取主服务器中的锁。
- 在对密钥的写入传输到副本之前,主服务器崩溃。
- 复制副本将升级为主副本。
- 客户端 B 获取 A 已为其持有锁的同一资源的锁。违反安全规定!
如何实现单个实例的Redis分布式锁
SET resource_name my_random_value NX PX 30000
仅在秘钥不存在时生成秘钥,并且其中的值my_random_value是全局唯一的,并设置过期时间30000ms
使用随机值是为了以安全的方式释放锁,并带有一个脚本告诉 Redis:仅当密钥存在并且存储在密钥中的值正是我期望的值时才删除密钥。这是通过以下 Lua 脚本完成的:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
使用lua脚本是为了保证语句的原子性;
防止误删key为了避免删除由其他客户端创建的锁,这一点很重要。例如,客户端可能会获取锁,在超过锁有效期(密钥过期的时间)的运行时间长于某些操作时被阻止,然后删除已由其他客户端获取的锁。 仅使用
DEL
是不安全的,因为客户端可能会删除另一个客户端的锁。使用上面的脚本,每个锁都使用随机字符串进行“签名”,因此只有当它仍然是客户端尝试删除它时设置的锁才会被删除。
这个随机字符串应该是什么?我们假设它是 的 20 个字节,但您可以找到更便宜的方法来使其对您的任务足够独特。 例如,一个安全的选择是用 为 RC4 提供种子,并从中生成伪随机流。 更简单的解决方案是使用具有微秒精度的 UNIX 时间戳,将时间戳与客户端 ID 连接起来。它不那么安全,但对于大多数环境来说可能已经足够了
Redlock算法
用来实现基于多个实例的分布式锁。
锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
该方案也是基于(set 加锁、Lua 脚本解锁)进行改良的,所以redis之父antirez 只描述了差异的地方,大致方案如下。
假设我们有N个Redis主节点,例如 N = 5这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,确保它们以几乎独立的方式失败。
为了取到锁客户端执行以下操作:
1 | 获取当前时间,以毫秒为单位; |
---|---|
2 | 依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个请求超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁; |
3 | 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功; |
4 | 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。 |
5 | 如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。 |
该方案为了解决数据不一致的问题,直接舍弃了异步复制只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。
客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
条件1:客户端从超过半数(大于等于N/2+1)的Redis实例上成功获取到了锁;
条件2:客户端获取锁的总耗时没有超过锁的有效时间。
N = 2X + 1 (N是最终部署机器数,X是容错机器数)
Redlock的实现之Redisson
Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)
加锁:
通过redisson新建出来的锁默认是30s过期时间
可重入:
采用hset,如果所不存在则创建锁并设置过期时间,如果key存在则锁的值递增,如果锁已存在但并非本线程则返回过期时间
续期:
额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间;
这里面初始化了一个定时器,dely 的时间是 internalLockLeaseTime/3。
在 Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s。
客户端A加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间,默认每次续命又从30秒新开始
自动续期的lua脚本