Redis 中如何实现分布式锁?

Sherwin.Wei Lv7

Redis 中如何实现分布式锁?

回答重点

在 Redis 中实现分布式锁的常见方法是通过set ex nx 命令 + lua 脚本组合使用。确保多个客户端不会获得同一个资源锁的同时,也保证了安全解锁和意外情况下锁的自动释放。

扩展知识

理解 Redis 实现的分布式锁

如果基于 Redis 来实现分布锁,需要利用 set ex nx 命令 + lua 脚本。

1)加锁:SET lock_key uniqueValue EX expire_time NX
2)解锁:使用 lua 脚本,先通过 get 获取 key 的 value 判断锁是否是自己加的,如果是则 del。

lua 脚本如下:

1
2
3
4
5
6
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end

我们来一一理解一下。

首先锁需要有过期机制。假设某个客户端加了锁之后宕机了,锁没有设置过期机制,会使得其他客户端都无法抢到锁。

EX expire_time 就是设置了锁的过期,单位是秒。还一个 PX, 也是过期时间,单位是毫秒。

然后,在 2.6.12 版本之前只有 SETNX 即 SET if Not eXists,它表示如果 key 已存在,则什么都不会做,返回 0,如果不存在则会设置它的值,返回 1。

那个时候 SETNX 和过期时间的设置就无法保证原子性,如果客户端在发送完 SETNX 之后就宕机了,还没来得及设置过期时间,一样会导致锁不会被释放。

因此 2.6.12 版本之后,优化了 SET 命令,使得可以执行 set ex px

最后,再解释下什么是 uniqueValue,翻译过来就是一个唯一的值。

之所以要设置唯一值是为了防止被别的客户端给释放了

我们来看下这个场景:

  1. 客户端 1 加锁成功,然后执行业务逻辑,但执行的时间超过了锁的过期时间
  2. 此时锁已经过期被释放了,客户端 2 加锁成功
  3. 客户端 2 执行业务逻辑
  4. 客户端 1 执行完了,执行释放锁的逻辑,即删除锁。

客户端 2 一脸懵,我还在执行着呢,怎么锁被人释放了??

所以每个客户端加锁(客户端可能是每个线程),需要是设置一个唯一标识,比如一个 uuid,防止锁被别的客户端误释放了。

因为需要先判断锁的值和唯一标识是否一致,一致后再删除释放锁,这里就涉及到两步操作,只有使用了 lua 脚本才能保证原子性,这也是为什么释放锁需要用 lua 脚本的原因。

单点故障问题

单台 Redis 实现分布式锁存在单点故障问题,如果采用主从读写分离架构,如果一个客户端在主节点上锁成功,但是主节点突然宕机,由于主从延迟导致从节点还未同步到这个锁,此时可能有另一个客户端抢到新晋升的主节点,此时会导致两个客户端抢到锁,产生了数据不一致。

基于这个情况,Redis 推出了 Redlock。

Redlock 红锁

Redlock 是 Redis 官方推荐的一种实现分布式锁的算法,适用于集群环境下。

Redlock 的基本思想

  • 部署多个 Redis 实例(通常为 5 个)。
  • 客户端在大多数实例(如至少 3 个)上请求锁,并在一定时间内获得成功,表示加锁成功。
  • 使用 Redlock 可以提供更高的容错性,即使部分 Redis 实例故障,仍然可以获得锁。

Redlock 的实现流程

  • 客户端尝试在每个 Redis 实例上加锁,必须在有限时间内(通常为锁的过期时间)完成所有实例的加锁。
  • 如果大多数实例(N/2 + 1)加锁成功,则表示加锁成功。
  • 否则,客户端将释放所有已经加锁的实例,重新尝试。

Redlock 的缺点包括

  • 复杂性:实现 Redlock 需要多个 Redis 实例,增加了系统的复杂性和维护成本。

  • 时间同步依赖:Redlock 依赖于多个节点的系统时间的一致性。如果节点之间的时间不同步(例如发生时间回拨),可能导致锁的有效性受到影响。

  • 不适用于高并发:在高并发场景下,因需要访问多个实例同时尝试获取锁,可能会导致锁获取的性能下降。

  • 锁的续期问题:在长时间的操作中,可能需要手动续期锁,因为涉及多个实例,增加了实现的复杂度和风险。

  • Redis 的 Red Lock 是什么?你了解吗?

Redis 分布锁注意事项以及其它常见分布式锁的实现

Comments