Skip to Content
DocumentationRedisredis实现分布式锁

Redis实现分布式锁

锁的实现需要什么条件

单机上的锁与分布式锁有很多相似之处,主要区别在于支持分布式场景。 单机锁锁定的资源只需要保证在当前机器进程上可见,分布式锁锁定的资源需要在分布式环境上所有机器进程可见。Redis本身是一个分布式环境下的共享存储系统,因此很适合用来实现分布式锁

客户端加锁和释放锁的操作逻辑,也和单机上的加锁和释放锁操作逻辑一致:

  1. 加锁时同样需要判断锁变量的值,根据锁变量值来判断能否加锁成功;
  2. 释放锁时需要把锁变量值设置为 0,表明客户端不再持有锁。

但是,和线程在单机上操作锁不同的是,在分布式场景下,锁变量需要由一个共享存储系统来维护,只有这样,多个客户端才可以通过访问共享存储系统来访问锁变量。相应的,加锁和释放锁的操作就变成了读取、判断和设置共享存储系统中的锁变量值。

这样一来,我们就可以得出实现分布式锁的两个要求。

  • 分布式锁的加锁和释放锁的过程,涉及多个操作。所以,在实现分布式锁时,我们需要保证这些锁操作的原子性;
  • 共享存储系统保存了锁变量,如果共享存储系统发生故障或宕机,那么客户端也就无法进行锁操作了。在实现分布式锁时,我们需要考虑保证共享存储系统的可靠性,进而保证锁的可靠性。

加锁与释放

前面得知,加锁与释放的操作,需要保证当前线程的操作是原子的,而在Redis上实现原子操作一般有两种方式:

  1. 使用单一命令
  2. 使用Lua脚本

由于加锁与释放是其实是对锁定资源的两种状态表达,因此我们可以用1来表达锁定,0表示未锁定。这样我们很容易通过SET命令进行加锁和释放。

SET lockKey 1; ##加锁 SET lockKey 0; ##释放锁

此时我们考虑到,当线程第一次持有锁时,这个key可能不存在,因此我们可以使用SETNX(如果key不存在则创建)命令替代

SETNX lockKey 1; ##加锁 SETNX lockKey 0; ##释放锁

这个时,一个基本的分布式锁就完成了,后续线程进入时,可以先判断lockKey对应的值是否为1,如果为1则表示已经有线程持有锁了,此时需要等待。 这个过程因为涉及到读写操作,因此可以用Lua脚本来执行

//释放锁 比较unique_value是否相等,避免误释放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("set",KEYS[1]) else return 0 end

等待队列

无论是分布式锁还是单机锁,都要求只有一个线程持有锁资源(这里前提是独占锁),其余线程需要进入自旋等待状态。 首选我们要给每个线程进行一个标识,该标识在整个分布式环境中是唯一的,一般来说的方案是机器id(mac地址)+pid+线程id 后续,当持有锁的线程释放锁时,所有等待线程进入抢占状态,此时抢占模式有两种:

  • 公平模式,严格按照进入等待时间由前到后的顺序抢占。
  • 非公平模式,各个线程同时抢占,那个线程先抢占到就持有。

非公平模式很简单,各个线程在自旋过程中不断判断加锁本身就是一种非公平模式。

公平模式的实现则需要构建一个FIFO的队列,Redis也提供了List集合,正好符合我们的需求。

  1. 当线程加锁失败进入等待队列时,可以使用List的rpush操作,将当前线程标识加入到list最后去
  2. 当之前线程释放锁后,再次尝试加锁时,需要判断当前线程与List的第一个线程是否一致,一致则尝试加锁,不一致则继续等待

续期

前面已经基本完成了一个分布式锁的构造,但我们还需要考虑一个问题:如果线程在持有锁的过程中,突然异常退出了,其他线程如何感知释放

解决方式也很简单:为每个lockKey设置一个过期时效,线程在持有锁后,不断的给lockKey刷新过期时效,这样当线程异常退出后,lockKey也可以正常释放

RedLock

按现有逻辑,完成的分布式锁是建立在单节点的Redis实例上的,一旦这个实例宕机了,分布式锁也就无法工作了,因此我们要尝试对分布式锁进行高可用方案。

为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock

  1. 第一步是,客户端获取当前时间。
  2. 第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作。
  3. 第三步是,一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时。

客户端只有在满足下面的这两个条件时,才能认为是加锁成功:

  • 客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;
  • 客户端获取锁的总耗时没有超过锁的有效时间。

在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。

当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作。

Last updated on