分布式锁

  • 可以通过使用同一个变量实现;加锁时候判断锁变量的值,根据锁变量的值来判断是否加锁成功;释放锁时需要把锁变量值设置为 0,表明客户端不再持有锁
  • 加锁和释放锁的操作也就是读取、判断、设置锁变量的值的过程

要求

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


加锁

  • 加锁包含了三个操作:读取锁变量、判断锁变量值、把锁变量值设置为 1,这三个操作在执行时需要保证原子性

如何保证原子性?

  • Redis 的单命令操作
  • 使用 Lua 脚本

Redis 的单命令操作

setnx 会读取、判断、设置锁变量

  • 如果 key 不存在则会被创建,值设置为 value
  • 如果 key 存在则不做任何赋值操作
setnx key value


// 改进增添过期时间 EX、PX、防止无法释放锁
// 表示 key 会在 10s 后过期
set key value NX PX 10000


释放锁

使用 DEL 命令删除锁变量

DEL key


// 改进使用 Lua 脚本执行、增添读取判断的业务逻辑
// 释放锁 先比较 value 是否相等,避免误释放
// KEYS[1]表示 key,ARGV[1]是当前客户端的唯一标识 value,
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end


完整业务逻辑

可以使用 setnx、del 命令组合来实现加锁和释放锁的操作

// 加锁
setnx key value
// 业务逻辑
do things
// 释放锁
DEL key

风险

  1. 客户端在执行了 setnx 命令加锁之后,紧接着却在操作共享数据时发生了异常,结果一直没有执行最后的 del 命令释放锁。因此锁就一直被这个客户端持有、其他客户端无法访问共享数据和执行后续操作,这会给业务应用带来影响
  2. 客户端 A 执行了 setnx 命令加锁之后,客户端 B 执行了 del 命令释放锁,导致客户端 A 的锁被误释放了。如果客户端 C 正好也在申请加锁就可以获得锁,进而开始操作共享数据。此时客户端 A、C 同时在对共享数据进行操作,数据就会被修改错误,这也是业务层不能接受的

解决

  1. 给锁变量设置一个过期时间,这样即使发生异常无法主动释放锁,锁变量也会在过期之后删除。其他客户端就可以正常请求加锁,不会出现无法加锁的问题了
// 增添过期时间 EX、PX、防止无法释放锁
// 表示 key 会在 10s 后过期
set key value NX PX 10000
  1. 将每个客户端的锁变量设置唯一值这个唯一值就可以用来标识当前操作的客户端。在释放锁操作时,客户端需要先判断,当前锁变量是否和自己的唯一标识相等,相同才会释放锁。这样就不会出现误释放锁的问题了
// 改进使用 Lua 脚本执行保证原子性、增添读取判断的业务逻辑
// 释放锁 先比较 value 是否相等,避免误释放
// KEYS[1]表示 key,ARGV[1]是当前客户端的唯一标识 value,
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end


扩展

基于多个 Redis 节点实现高可靠的分布式锁

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

RedLock 算法的基本思路

  • 让客户端和多个独立的 redis 实例依次请求加锁,如果客户端能过和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获取了分布式锁,否则加锁失败
  • 这样即使单个 Redis 实例发生故障,锁变量在其他实例上也有保存,所以客户端仍然可以正常地进行锁操作,锁变量并不会丢失

RedLock 算法的步骤

  1. 客户端获取当前时间
  2. 客户端按顺序依次向 N 个 Redis 实例执行加锁操作(加锁操作的超时时间需要远远地小于锁的有效时间)
  3. 一旦客户端完成了所有 redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时

只有满足这两个条件,才认为是加锁成功
  • 客户端从超过半数的 redis 实例上成功获取到锁
  • 客户端获取锁的总耗时没有超过锁的有效时间

后续
  • 成功后更新锁的有效时间,最初的有效时间 - 客户端获取锁的总耗时
  • 如果客户端在所有实例执行完加锁操作后,没能同时满足这两个条件,那么客户端向所有 redis 节点发起释放锁的操作


参考