Redis 实现分布式锁

1. 前言

在许多情形中,不同的进程必须以 互斥 的方式对共享资源进行操作时,这时候分布式锁就非常有用了。让我们来看看 Redis 官网中是如何实现的DLM (Distributed Lock Manager)

1.1 什么是分布式锁呢?

分布式锁是控制分布式系统之间同步访问共享资源的一种方式,要求控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。

超级推荐这篇,看了绝对懂了什么是分布式锁 史上关于分布式锁最全问题汇总

总有人问什么是分布式锁,看完就懂了

单体架构如下:
在这里插入图片描述
图中的Redis和数据库是共享资源,以前的单体架构应用,要实现对共享资源的一致性,还用不到分布式锁,只需要使用线程锁就行了。
在这里插入图片描述
但是,在分布式架构中,用简单的线程锁就不行了,应用在不同的机器上,用线程锁只能控制1台机器中的线程访问共享资源游学,控制不了多台。为了保证互斥性(保证同一时刻只有一个线程能够修改该资源),所以在上锁的时候加了一个唯一的随机值(不同的线程对应不同的随机值),只有这个随机值匹配的情况下才可以解锁。

所以,出现了分布式锁。

在这里插入图片描述

分布式锁有很多实现方法,通过 数据库,Memcached、Redis、Zookeeper、Chubby等都可以实现分布式锁。

什么是分布式锁
什么是分布式锁?实现分布式锁的三种方式

在使用分布式锁的过程中需要保证以下3点(说的更细点应该是6点,也在下面):

  1. 安全性。互斥。在任何确定的时刻,只能有一个client能够持有锁。
  2. 无死锁。即使持有锁的client崩溃了或者分区了,其它 client仍然能够获得锁。
  3. 容错性。只要大多数 Redis 节点是好使的,clients 就应该能够获取和释放锁。

分布式锁应该具备哪些条件:

在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
高可用的获取锁与释放锁
高性能的获取锁与释放锁
具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
具备锁失效机制,防止死锁
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

在下面不管是单机模式主从模式,还是Sentinel模式,还是cluster模式,都只是在实现分布式锁的过程中,Redis的不同部署方式而已。

Redis的部署方式:

  • 单机模式。会出现单点故障。
  • 主从模式。可以手动实现故障转移。主从切换存在锁丢失情况。
  • Sentinel模式。可以自动实现故障转移。主从切换存在锁丢失情况。
  • Redis Cluster模式。向集群中的每一个Redis节点获取锁,当获得了一般一上的节点的锁,表明获取锁成功。

2. 仅仅实现故障转移还不够

Redis中要锁定一个资源很简单,在某个Redis实例上创建一个key就行了。这个key通常都要设置生存时间(time to live,简称TTL),也就是过期时间。以达到最终一定会释放锁的目的(避免产生死锁)。如果你要释放锁,只需要删除这个key就行。

但是,如果只使用一台Redis实例,那么很容易出现单点故障。所以,我们为这台 Redis master添加一个slave。但是又有一个问题,Redis masterslave上的数据是异步的(也就是说,在某一时刻,两者上的数据可能是不一致的)。如下所示:

  1. client Amaster 获取锁。
  2. mastersetkey 同步到slave 之前,master就挂了。
  3. slave成为新的master
  4. client B 获取同一个资源的锁,但其实这个资源的锁已经被 client A锁持有了,这违背了安全性。

3. 单机Redis实现分布式锁

获取锁

SET resource_name my_random_value NX PX 30000

上面这个命令只有在key 不存在时才会 set keyNX选项),并且过期时间设置为30000毫秒(PX选项)。我们将这个key 的值 set为了一个唯一的随机值,避免与其他 client 所要 set的值相同。

这个随机值的作用是用来以更安全的方式释放锁。如下面的LUA脚本所示,只有在这个key已经存在且这个随机值是我们想要的时,才能删除这个key

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

上面这段脚本就是为了避免一个client的锁被其它client释放(因为其他client的随机值和我们想要的不一样)。例如,client A已经获取了锁,但是由于某些阻塞操作导致时间超过了锁的有效时间,然后client A想要释放锁,但是其实这时候该锁已经被client B获取了。如果直接使用DEL删除key,那么就会是 client A 删除了 client B 所创建的key。然而,使用上面的LUA脚本就不会出现这种情况。

这个随机值可以有很多算法产生。简单的可以是unix时间戳加上clientid,这个在大多数情况下还是挺安全的。

key 的生存时间(time to live)也就是锁的有效时间。

单机Redis的缺点就是会出现单点故障

4. Redlock 算法

我们假定有N(令N=5)个Redis masters。这些节点之间相互独立(没有副本,或者协调系统,应该说的是zookeeper这类东西)。这5个Redis masters运行在不同的计算机或者虚拟机上。

获取锁的过程如下:

  1. 获取当前时间戳的毫秒值。
  2. 尝试依次在N个Redis实例中使用相同key名和随机值获取锁。client使用的超时时间要小于锁的自动释放时间(为了一定能释放锁)。
    3.client计算获取锁花费了多少时间(当前时间戳 - 第一步的时间戳)。只有当client获取到至少3个实例的锁(n/2 +1),并且获取锁花的时间小于锁的有效时间,才认为这个锁被此client获取了。
  3. 如果这个锁被获取了,锁的剩余有效时间 = 初始有效时间 - 过去的时间。
  4. 如果由于某些原因(没有获取到 n/2+1个Redis实例的锁,或者过了有效时间),client 获取锁失败了,应当尝试对所有Redis实例进行解锁(哪怕这个Redis实例并没有上锁)

5. 算法是异步的吗?

这个算法的假设条件是:

尽管各进程之间没有同步时钟,但每个进程中的本地时间基本一致,并且与锁的自动释放时间相比,误差较小。 这个假设与现实世界的计算机非常相似:每台计算机都有一个本地时钟,不同的计算机之间会有一个很小的时钟漂移。所以,实际上的锁的有效时间还要扣除时钟漂移的部分。

6. 失败后重试

当一个client获取锁失败后,应当在随机时间延迟后进行重试(使用随机延迟,是为了尽量避免在同一时刻获取同一资源,最终导致没有人能够抢占到锁)。较快的client会尝试获取大多数的Redis实例的锁,理想情况下,应当以多路复用策略向N个Redis实例同时发送 SET命令。

需要注意的是,如果client没有获取到 大多数Redis实例的锁,需要尽快释放锁,避免等到锁自动释放之后才能获取(但是如果发生网络分区或者client无法与该Redis实例通信,那么就无法释放该锁,降低可用性,甚至造成经济损失)。

7. 释放锁

释放锁就很简单了,只需在所有实例中释放锁,无论client是否认为它能够成功锁定给定的实例。

8. 安全性讨论

上面的算法安全不?

首先,假设一个client 已经获取了大多数Redis实例的锁。所有的Redis实例都包含一个相同TTLkey。然而,不同的Redis实例是在不同的时间点set key,所以这些key也是在不同的时间点过期。例如,第一个Redis实例的key是在T1时刻set的,最后一个Redis实例的key是在T2时刻set的,那么我们可以保证的是第一个Redis实例的key至少存在MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT。然后其他实例上的key也会逐渐过期,我们所能保证就是在MIN_VALIDITY这段时间内,该key在所有Redis实例中是都存在的。

在当大多数Redis实例的keyset后,其它client将无法获取锁,因为如果已经获取了N / 2 + 1个Redis实例的key,则其它client不可能在获取到N / 2 + 1个Redis实例的key。 因此,如果锁已经被获取了,则不可能同时重新获取它(违反互斥)。

如果在MIN_VALIDITY时间内,其它client 不能够获取此锁。 因此,只有当大多数Redis实例的已经锁定的时间大于TTLclient才可以同时获得N / 2 + 1个Redis实例的锁),从而使锁定无效。

9. 可用性讨论

系统可用性基于以下3点:

  1. 自动释放锁(因为key会过期)。
  2. 通常情况下,client会在未获得锁或获得锁且工作终止时删除锁,这使得我们不必等待key过期就可以重新获得该锁。
  3. client重试获取锁时,它等待的时间要比获取大多数Redis实例的锁所需的时间长得多,以便避免在资源争用期间最终无人获得锁的情况出现。

但是如果一个client超过了锁的有效时间,还没执行完跑完相关代码呢?所以,需要分布式锁是可重入锁。具体见 redisson 的实现。

10. 性能,崩溃恢复和fsync

为了满足更高的性能要求,采用多路复用策略(或简单的多路复用,以套接字的非阻塞模式,想所有Redis实例发送命令,并读取所有回复)来降低与N个Redis服务器进行通信的延迟。

对于崩溃恢复,我们还需要考虑持久化

如果我们不采用持久化来配置Redisclient A 获得了5个Redis实例中的3个的锁。然后这3个中的一个Redis实例重启了。此时,client B 又可以获取3个Redis实例的锁了,这就违反了互斥原则。

如果启用AOF持久性,则情况会大大改善。

总结

本质:用Redis实现锁,就是将要锁的资源名作为key,存在Redis中,client看看这个资源名的key是否存在,存在就说明有人在用了,就认为上锁了,其他client此时不能修改此资源。

由于本博客是对 Redis官方文档 分布式锁 的翻译,可能会难以理解,大家可以看看下面的博客,我觉得写得挺好的。

史上关于分布式锁最全问题汇总
分布式锁之Redis实现
redis分布式锁原理及实现
Redis官方文档 分布式锁

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值