是谁,又被分布式锁给锁住了?(下)

61 阅读3分钟

大家好,我是徒手敲代码。

上篇文章提到使用 Redis 来实现分布式锁,针对过期时间长短,以及锁被其他线程释放这两个问题,分别都提出了各自的解决方案。

这些解决方案,用在单机部署的 Redis,确实没什么问题。但是一般在使用 Redis 的时候,都会采用主从集群 + 哨兵的方式去部署,这样做可以保证 Redis 服务的高可用,即使主节点挂了,也可以在从节点里面再选一个出来,作为主节点。

当发生主从切换的时候,这个分布式锁就变得不安全了,为什么呢?

试想一下这个场景:

1、客户端在主节点上执行set命令,加锁成功

2、加锁成功之后的那一刻,主节点挂了,此时数据还没有同步到从节点

3、从节点被选举为新的主节点,而新的主节点并不知道有这个锁的存在

这样一顿操作下来,锁就丢失了。那么这个问题怎么解决呢?

Redis 的作者提出了一个解决方案,叫做 Redlock。这个方案的前提是,部署了至少 5 个Redis实例,它们是毫无关联的,并且都是主库。

整个加锁过程包括以下几步:

  • 获取当前时间戳 T1
  • 客户端按顺序向 Redis 实例发起加锁请求,每个加锁请求都会设置网络过期时间,以及锁的过期时间,如果超过网络过期时间,还没有加锁成功,就立刻向下一个Redis 实例发请求
  • 如果客户端成功在大多数(>= N/2+1) Redis 实例上成功获取锁,就获取当前时间戳 T2,判断T2-T1的值是否小于锁的过期时间,也就是判断,在后面的实例成功加锁之后,前面加的锁有没有过期,如果没过期,那么就认为加锁成功
  • 加锁成功之后,客户端可以去操作共享资源
  • 操作完成之后,向所有 Redis 实例发起释放锁请求,使用Lua脚本确保原子性。

解释一下这些步骤背后的原因:

为什么要在多个节点上加锁?

5个或以上的 Redis 实例,就相当于一个分布式系统,即使有一部分节点挂了,整个系统仍然可用,锁的状态仍然是可靠的。

为什么要限制每个加锁请求的时间?

为了防止客户端因为某个节点的网络问题,而陷入无线等待,降低性能。

为什么最后要对所有节点释放锁,而不是只操作加锁成功的节点?

举个例子,客户端在某个 Redis 实例加锁成功,但是因为网络问题,一直没有返回成功的信息,导致客户端误认为这个实例加锁失败,如果最后这个锁没有被释放,那么锁将一直残留在这个 Redis 实例当中。

所以这个步骤可以保证,清理掉所有残留的锁。

除了 Redis,分布式锁还可以用Zookeeper、Etcd 来实现,篇幅问题,这里就不进行展开了。无论是用哪种技术来实现,都始终不能保证百分百的安全,需要有兜底的技术方案,比如在数据库层面使用版本号的方式,来更新数据,避免发生冲突。

今天的分享到这里结束了。

关注公众号“徒手敲代码”,免费领取腾讯大佬推荐的Java电子书!