在许多情形中,不同的进程必须以 互斥
的方式对共享资源进行操作时,这时候分布式锁就非常有用了。让我们来看看 Redis
官网中是如何实现的DLM (Distributed Lock Manager)
。
那么什么是分布式锁呢?
分布式锁就是一种思想。这种思想要求控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。
总有人问什么是分布式锁,看完就懂了
以前的单体架构应用,要实现对共享资源的一致性,还用不到分布式锁,只需要使用线程锁就行了(保证同一时刻只有一个线程能够修改该资源)
但是,在分布式架构中,用简单的线程锁就不行了,为了保证互斥性(保证同一时刻只有一个线程能够修改该资源),所以在上锁的时候加了一个唯一的随机值(不同的线程对应不同的随机值),只有这个随机值匹配的情况下才可以解锁。
分布式锁有很多实现方法,通过 数据库,Memcached、Redis、Zookeeper、Chubby等都可以实现分布式锁。
什么是分布式锁
什么是分布式锁?实现分布式锁的三种方式
在使用分布式锁的过程中需要保证以下3点(说的更细点应该是6点,也在下面):
安全性
。互斥。在任何确定的时刻,只能有一个client
能够持有锁。无死锁
。即使持有锁的client
崩溃了或者分区了,其它 client
仍然能够获得锁。容错性
。只要大多数 Redis
节点是好使的,clients
就应该能够获取和释放锁。分布式锁应该具备哪些条件:
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
高可用的获取锁与释放锁
高性能的获取锁与释放锁
具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
具备锁失效机制,防止死锁
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
在Redis
中要锁定一个资源很简单,在某个Redis
实例上创建一个key
就行了。这个key通常都要设置生存时间(time to live,简称TTL)
,也就是过期时间。以达到最终一定会释放锁的目的(避免产生死锁
)。如果你要释放锁,只需要删除这个key
就行。
但是,如果只使用一台Redis
实例,那么很容易出现单点故障
。所以,我们为这台 Redis master
添加一个slave
。但是又有一个问题,Redis master
和 slave
上的数据是异步的(也就是说,在某一时刻,两者上的数据可能是不一致的)。如下所示:
client A
从 master
获取锁。master
将set
的key
同步到slave
之前,master就挂了。slave
成为新的master
。client B
获取同一个资源的锁,但其实这个资源的锁已经被 client A
锁持有了,这违背了安全性。获取锁
SET resource_name my_random_value NX PX 30000
上面这个命令只有在key
不存在时才会 set key
(NX
选项),并且过期时间设置为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
时间戳加上client
的id
,这个在大多数情况下还是挺安全的。
key
的生存时间(time to live
)也就是锁的有效时间。
这样对于非分布式系统而言,我们有了一个较好的方法来获取和释放锁。那么在分布式系统中应该如何来做呢?
我们假定有N(令N=5)个Redis masters
。这些节点之间相互独立(没有副本,或者协调系统,应该说的是zookeeper
这类东西)。这5个Redis masters
运行在不同的计算机或者虚拟机上。
获取锁的过程如下:
Redis
实例中使用相同key
名和随机值获取锁。client
使用的超时时间要小于锁的自动释放时间(为了一定能释放锁)。client
计算获取锁花费了多少时间(当前时间戳 - 第一步的时间戳)。只有当client
获取到至少3个实例的锁(n/2 +1),并且获取锁花的时间小于锁的有效时间,才认为这个锁被此client
获取了。Redis
实例的锁,或者过了有效时间),client
获取锁失败了,应当尝试对多有Redis
实例进行解锁(哪怕这个Redis
实例并没有上锁)这个算法的假设条件是:
尽管各进程之间没有同步时钟,但每个进程中的本地时间仍以大约相同的速率流动,并且与锁的自动释放时间相比,误差较小。 这个假设与现实世界的计算机非常相似:每台计算机都有一个本地时钟,我们通常可以依靠不同的计算机来产生很小的时钟漂移
。所以,实际上的锁的有效时间还要扣除时钟漂移
的部分。
当一个client
获取锁失败后,应当在随机时间延迟后进行重试(之所以,使用随机延迟
,是为了尽量避免在同一时刻获取同一资源,最终导致没有人能够抢占到锁)。较快的client
会尝试获取大多数的Redis
实例的锁,理想情况下,应当以多路复用策略
向N个Redis
实例同时发送 SET
命令。
需要注意的是,如果client
没有获取到 大多数Redis
实例的锁,需要尽快释放锁,避免等到锁自动释放之后才能获取(但是如果发生网络分区或者client
无法与该Redis
实例通信,那么久无法释放该锁,降低可用性,甚至造成经济损失)。
释放锁就很简单了,只需在所有实例中释放锁,无论client
是否认为它能够成功锁定给定的实例。
上面的算法安全不?
首先,假设一个client
已经获取了大多数Redis
实例的锁。所有的Redis
实例都包含一个相同TTL
的key
。然而,不同的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
实例的key
被set
后,其它client
将无法获取锁,因为如果已经获取了N / 2 + 1个Redis
实例的key
,则其它client
不可能在获取到N / 2 + 1个Redis
实例的key
。 因此,如果获取了锁,则不可能同时重新获取它(违反互斥)。
如果在MIN_VALIDITY
时间内,其它client
不能够获取此锁。 因此,只有当大多数Redis
实例的已经锁定的时间大于TTL
,client
才可以同时获得N / 2 + 1个Redis
实例的锁),从而使锁定无效。
系统可用性基于以下3点:
key
会过期)。client
会在未获得锁或获得锁且工作终止时删除锁,这使得我们不必等待key
过期就可以重新获得该锁。client
重试获取锁时,它等待的时间要比获取大多数Redis
实例的锁所需的时间长得多,以便避免在资源争用期间最终无人获得锁的情况出现。为了满足更高的性能要求,采用多路复用策略
(或简单的多路复用,以套接字的非阻塞模式,想所有Redis
实例发送命令,并读取所有回复)来降低与N个Redis
服务器进行通信的延迟。
对于崩溃恢复,我们还需要考虑持久化
。
如果我们不采用持久化
来配置Redis
。client A
获得了5个Redis
实例中的3个的锁。然后这3个中的一个Redis
实例重启了。此时,client B
又可以获取3个Redis
实例的锁了,这就违反了互斥原则。
如果启用AOF持久性
,则情况会大大改善。
由于本博客是对 Redis官方文档 分布式锁 的翻译,可能会难以理解,大家可以看看下面的博客,我觉得写得挺好的。
分布式锁之Redis实现
redis分布式锁原理及实现
Redis官方文档 分布式锁