Redis分布式锁:原理、问题与解决方案深度剖析

在这里插入图片描述

一、引言

在分布式系统的项目开发中,基于 Redis 实现分布式锁是常见的业务需求。不过,许多实现方案并未充分考虑分布式环境下可能出现的复杂问题。接下来,我们将深入探讨 Redis 分布式锁的相关知识。

二、锁的基本概念

(一)什么是锁

简单来说,锁就像是一个“关卡守卫”,是多个线程判断能否访问资源的关键参考点。当一个线程想要对数据进行写入操作时,它得先看看是否有写锁存在。要是有写锁,那就只能乖乖等待,直到锁被释放,自己拿到锁后才能执行写入操作。这样做的目的很明确,就是防止多个线程同时写入,避免数据损坏等糟糕情况的发生。

(二)为什么需要锁

在实际业务场景中,引入锁主要是为了确保在多个可能执行相同任务的节点里,同一时间只有一个节点能真正执行操作。这些操作可能包括将数据写入共享存储系统、进行复杂计算或者调用外部 API 等。引入锁的原因通常有两个方面:

  1. 效率方面:使用锁可以避免重复做相同的工作,比如一些高成本的计算。要是锁机制失效,两个节点都去做同样的工作,就会增加成本,还可能给用户带来不好的体验。
  2. 正确性方面:锁能防止并发进程相互干扰,从而保证系统状态的稳定。一旦锁机制出现问题,两个节点同时处理同一条数据,就可能引发文件损坏、数据丢失、数据不一致等严重问题,甚至损害用户权益。

三、分布式锁的应用与属性

(一)分布式锁的应用

在保证程序操作正确性的场景下,系统里有些资源不能被多个进程同时使用。就像一个文件不能被多个进程同时更新,打印机同一时间只能被一个进程使用。为了确保进程对这类共享资源的独占访问,也就是实现“进程间互斥”,我们会使用分布式锁来控制对资源的访问。那些需要独占访问共享资源的程序部分被称为临界区。

(二)分布式锁的基本属性

要实现一个合格的分布式锁服务,需要满足以下几个重要属性:

  1. 互斥性:在任何时刻,只能有一个客户端持有锁,这是分布式锁最基本的要求。
  2. 无死锁:每个锁请求最终都能得到处理,即便持有锁的客户端崩溃或者出现异常,也不会导致死锁的情况发生。
  3. 容错性:只要大部分 Redis 节点能够正常运行,客户端就可以进行加锁和解锁操作。

四、分布式锁的缺陷

(一)锁失效问题

客户端长时间阻塞可能会导致锁失效。举个例子,假设客户端 1 成功获取了锁,但由于网络故障或者垃圾回收(GC)等原因,客户端 1 出现长时间阻塞。在业务程序还没执行完的时候,锁就过期了。这时,客户端 2 就能正常获取到锁,这就可能引发线程安全问题。

(二)时钟漂移问题

如果 Redis 服务器的时钟向前跳跃,就会使锁的 Key 过早超时失效。比如,客户端 1 获取到锁后,设置 Key 的过期时间是 10:02 分,但 Redis 服务器的时钟比客户端快了 2 分钟,那么 Key 可能在 10:00 就失效了。要是此时客户端 1 还没释放锁,就会出现多个客户端同时持有同一把锁的情况。

(三)单点安全问题

当 Redis 集群采用单 Master 模式时,如果这台 Master 服务器宕机,所有客户端都可能无法获取到锁。为了提高可用性,我们会给 Master 引入 Slave 节点。但由于 Redis 的主从同步是异步进行的,可能会出现这样的情况:客户端 1 设置好锁后,Master 突然挂掉,Slave 晋升为新的 Master,而由于异步复制的特性,客户端 1 设置的锁丢失了。这时,客户端 2 也能成功设置锁,导致客户端 1 和 2 同时拥有同一把锁。

五、分布式锁的不同实现策略

(一)Chubby 分布式锁

Chubby 分布式锁源自 Google 公司,是一种粗粒度的分布式锁服务。它和 ZooKeeper 有相似之处,但也存在较大差异。Chubby 通过 Sequencer 机制来解决请求延迟导致的锁失效问题。

(二)Zookeeper 分布式锁

Zookeeper 分布式锁基于其顺序临时节点来实现分布式锁和队列等待。Zookeeper 是专门为分布式应用设计的框架,提供了很多实用的特性,比如 ephemeral 类型的 znode 会自动删除,还提供了 Watch 机制。这使得分布式锁在客户端使用起来就像本地锁一样,加锁失败时会自动阻塞,直到获取到锁为止。

(三)Consul 分布式锁

Consul 分布式锁是基于 Consul 的 Key / Value 存储 API 中的 Acquire 和 Release 操作实现的。这两个操作类似于 Check - And - Set 操作:

  • Acquire 操作只有在锁没有持有者时才会返回 True,同时设置 Value 值,执行操作的 Session 会持有该 Key 的锁;否则返回 False。
  • Release 操作使用指定的 Session 来释放某个 Key 的锁,如果指定的 Session 无效,返回 False;否则设置 Value 值并返回 True。

(四)Redis 分布式锁

1. 单机实现

基于 Redis 单机的分布式锁利用 SETNX 命令,该命令具有原子性,只有当 Key 不存在时才能设置成功。

Key不存在
相等
Key存在
客户端尝试加锁
检查Key是否存在SETNX命令
设置Key成功获取锁
为锁设置过期时间
执行临界区业务逻辑
使用完后客户端解锁
比较唯一标识是否相等
执行删除操作释放锁
获取锁失败等待或重试
2. 集群实现(Redlock)

Redis 的作者 antirez 提出了 Redlock 算法,用于在 Redis 集群环境下实现更安全有效的分布式锁。具体流程如下:

  • 第一步,获取当前时间。
  • 第二步,依次向 N 个节点获取锁,并设置响应超时时间,防止在单个节点上获取锁的时间过长。
  • 第三步,计算锁有效时间,即锁过期时间减去获取锁所耗费的时间。如果在第二步中成功获取锁的节点数超过 N/2 + 1,并且锁有效时间大于 0,则表示获取锁成功。
  • 第四步,如果获取锁失败,需要向所有节点释放锁。

简单概括就是,在锁的过期时间内,如果超过半数的节点成功获取到锁,就认为获取锁成功,这有点类似于 ZooKeeper 的选举机制。

开始
获取当前时间
依次向N个节点获取锁设置响应超时时间
获取锁的节点数是否超过N/2 + 1且锁有效时间>0
获取锁成功执行临界区业务逻辑
执行完业务后向所有节点释放锁
结束
获取锁失败向所有节点释放锁

六、Redis 分布式锁场景剖析

(一)加锁操作的正确姿势

如果要基于 Redis 实现分布式锁,加锁操作需要遵循以下要点:

  1. 使用 Setnx 命令保证互斥性,确保同一时间只有一个客户端能获取到锁。
  2. 为锁设置过期时间,避免出现死锁的情况。
  3. 要保证 Setnx 和设置过期时间这两个操作的原子性,防止在设置 Setnx 成功后,客户端在设置过期时间时崩溃,从而导致死锁。
  4. 加锁的 Value 值应该是一个唯一标识,可以使用 UUID 来实现。加锁成功后,要将这个唯一标识返回给客户端,用于后续的解锁操作。

(二)解锁操作的正确姿势

解锁操作也有相应的规范:

  1. 解锁时需要使用加锁成功时的唯一标识,确保加锁和解锁的是同一个客户端。
  2. 解锁操作要先比较唯一标识是否相等,相等的情况下再执行删除操作。可以使用 Lua 脚本实现这两个命令的原子性。
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐