实现分布式锁的两个要求
- 分布式锁的加锁和释放锁的过程,涉及多个操作。所以,在实现分布式锁时,我们需要保证这些锁操作的原子性
- 共享存储系统保存了锁变量,如果共享存储系统发生故障或宕机,那么客户端也就无法进行锁操作了。在实现分布式锁时,我们需要考虑保证共享存储系统的可靠性,进而保证锁的可靠性。
基于单个 Redis 节点实现分布式锁
单命令操作实现加锁操作
- SETNX 命令
- 用于设置键值对的值
- 这个命令在执行时会判断键值对是否存在
- 如果不存在,就设置键值对的值
- 如果存在,就不做任何设置
- 可以用 SETNX 和 DEL 命令组合来实现加锁和释放锁操作
使用 SETNX 和 DEL 命令组合实现分布锁,存在两个潜在的风险
- 客户端在执行了 SETNX 命令、加锁之后,异常崩溃,不能执行 DEL 命令释放锁
- 解决方法:给锁变量设置一个过期时间
- 如果客户端 A 执行了 SETNX 命令加锁后,假设客户端 B 执行了 DEL 命令释放锁,此时,客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁,就可以成功获得锁,进而开始操作共享数据。
- 解决方法:需要能区分来自不同客户端的锁操作
- 在加锁操作时,可以让每个客户端给锁变量设置一个唯一值,这里的唯一值就可以用来标识当前操作的客户端。在释放锁操作时,客户端需要判断,当前锁变量的值是否和自己的唯一标识相等,只有在相等的情况下,才能释放锁。
- 解决方法:需要能区分来自不同客户端的锁操作
为了能达到和 SETNX 命令一样的效果,Redis 给 SET 命令提供了类似的选项 NX,用来实现“不存在即设置”。
- 如果使用了 NX 选项,SET 命令只有在键值对不存在时,才会进行设置,否则不做赋值操作
- SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间
执行下面的命令时,只有 key 不存在时,SET 才会创建 key,并对 key 进行赋值。另外,key 的存活时间由 seconds 或者 milliseconds 选项值来决定
|
|
|
|
- unique_value 是客户端的唯一标识,可以用一个随机生成的字符串来表示
- PX 10000 则表示 lock_key 会在 10s 后过期
因为在加锁操作中,每个客户端都使用了一个唯一标识,所以在释放锁操作时,我们需要判断锁变量的值,是否等于执行释放锁操作的客户端的唯一标识
|
|
在释放锁操作中,使用了 Lua 脚本,这是因为,释放锁操作的逻辑也包含了读取锁变量、判断值、删除锁变量的多个操作,而 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
基于多个 Redis 节点实现高可靠的分布式锁
为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。
Redlock 算法的执行步骤
- 第一步是,客户端获取当前时间
- 第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作
- 第三步是,一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时
- 客户端只有在满足下面的这两个条件时,才能认为是加锁成功
- 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁
- 条件二:客户端获取锁的总耗时没有超过锁的有效时间
- 在满足了这两个条件后,需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
- 如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作
- 客户端只有在满足下面的这两个条件时,才能认为是加锁成功
在 Redlock 算法中,释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。这样一来,只要 N 个 Redis 实例中的半数以上实例能正常工作,就能保证分布式锁的正常工作了。
在实际的业务应用中,如果你想要提升分布式锁的可靠性,就可以通过 Redlock 算法来实现。