Redlock与一些记录
Redis与分布式锁
SET NX
锁通常是为共享资源的访问提供互斥能力,分布式锁适用于为分布式环境下的共享资源的并发访问,通常是在分布式的共享资源自身无法提供互斥能力的情况下(如分布式文件系统等)。
客户端访问分布式共享资源的步骤一般是:1. 尝试加锁;2. 加锁成功则可以进行共享资源访问;3. 解锁。其中,加锁和解锁操作需要具有原子性,不会出现加锁加到一半或者解锁解到一半的情况。
不具备原子性的加锁/解锁操作会造成一些问题,如客户端1加锁只进行了一半,另外一个客户端2也尝试加锁,由于客户端1还未成功,所以客户端2也可进行加锁操作,最终可能造成客户端1和2同时持有同一个锁的情况。
为什么不在尝试加锁之前检查有没有其它加锁操作正在进行呢?
Redis 为 SET
操作提供了可选的 NX
参数,实现若不存在则设置的原子操作,可以利用SET NX
来实现分布式锁。而解锁时,只需要使用DEL
将对应的KEY删除即可。
1 | func setNxExample1() { |

上图是使用SET NX
来实现分布式锁的示例情况:
- 客户端1使用
SET NX
获取锁,由于KEY不存在,加锁成功; - 客户端2使用
SET NX
获取锁,由于KEY已存在,加锁失败; - 客户端1访问共享资源;
- 客户端1使用
DEL
解锁; - 客户端2使用
SET NX
获取锁,由于KEY已删除,加锁成功。
过期时间
考虑一种情况,如果加锁的客户端因为一些异常挂掉就会产生死锁,那么其它客户端永远也无法获取到该锁,无法访问共享资源。下图描述了这种问题,客户端2永远也无法加锁。

因此不能让锁被某个客户端永久持有,一种最简单的策略就是给锁加上过期时间,从而使锁的持有变为一段有限时间的租约,保证了锁的活性。如下图所示,在客户端1的租约到期以后,客户端2可以成功加锁并访问共享资源。

客户端标识
由于增加了过期时间,租约到期可能会引起锁的误删,下图描述了这种错误可能发生的场景。

- 客户端1首先加锁,并成功访问到了资源,但由于延时过长,导致锁过期;
- 客户端2在客户端1加的锁过期之后访问,因此成功加锁;
- 客户端1此时做完操作,使用
DEL
命令解锁,将客户端2的锁误删; - 客户端1又重新获取锁,之前的锁已被删除,因此加锁成功;
- 此时客户端1和2都认为自己已经进行加锁,出现两者同时访问互斥资源的情况,错误发生。
这是由于Redis并不会检查解锁的客户端和加锁的客户端是否为同一个,为了解决这个问题,就需要在删除前能够判断锁是否仍然归删锁者所有。一个常见的做法是,将锁的value设置为客户端的唯一标识(如UUID),在删除锁时,先检查值是否一致,一致才删除。需要注意的是,需要保证检查+删除操作具有原子性,因此一般需使用Lua脚本来保证。

以下是综合考虑之后得到的使用示例:
1 | func setNxExample2() { |
Redlock
主从复制Redis与脑裂
Redis的主从复制集群模式可能会发生脑裂问题:主服务器由于网络问题失去了和其它服务器的连接,但和客户端的连接正常。这时哨兵会重新选举出一个主服务器,当原主服务器恢复连接后,需要全量同步新主服务器的数据。这个问题会造成客户端在网络断连期间在原主服务器上所做的变更丢失。

脑裂问题实际上就是CAP理论中的出现了网络分区的情况,上面描述的即是放弃一致性,保证分区容错性和可用性的方案。也有放弃可用性的方案,即当Redis的主服务器发现当前有超过一定阈值数量的从服务器断连或超时,就拒绝客户端发起的变更请求并返回错误。
Redlock实现
对于基于主从复制的Redis的分布式锁而言,脑裂问题会造成锁的丢失。为了应对这样的问题,实现基于Redis集群的分布式锁,提出了Redlock算法。
Redlock要求部署奇数个Redis示例(官方推荐5个),具体的流程如下图所示:

算法假设:NPC(分布式系统的三种问题:Network Delay、Processor Pause、Clock Drift)造成的误差相比于TTL(锁的有效期)来说是非常小的。