30 | 如何使用Redis实现分布式锁?

极客时间 | 《Redis核心技术与实战》学习笔记目录

原文

实现分布式锁的两个要求

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

基于单个 Redis 节点实现分布式锁

单命令操作实现加锁操作

  • SETNX 命令
    • 用于设置键值对的值
    • 这个命令在执行时会判断键值对是否存在
      • 如果不存在,就设置键值对的值
      • 如果存在,就不做任何设置
  • 可以用 SETNX 和 DEL 命令组合来实现加锁和释放锁操作

使用 SETNX 和 DEL 命令组合实现分布锁,存在两个潜在的风险

  • 客户端在执行了 SETNX 命令、加锁之后,异常崩溃,不能执行 DEL 命令释放锁
    • 解决方法:给锁变量设置一个过期时间
  • 如果客户端 A 执行了 SETNX 命令加锁后,假设客户端 B 执行了 DEL 命令释放锁,此时,客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁,就可以成功获得锁,进而开始操作共享数据。
    • 解决方法:需要能区分来自不同客户端的锁操作
      • 在加锁操作时,可以让每个客户端给锁变量设置一个唯一值,这里的唯一值就可以用来标识当前操作的客户端。在释放锁操作时,客户端需要判断,当前锁变量的值是否和自己的唯一标识相等,只有在相等的情况下,才能释放锁。

为了能达到和 SETNX 命令一样的效果,Redis 给 SET 命令提供了类似的选项 NX,用来实现“不存在即设置”。

  • 如果使用了 NX 选项,SET 命令只有在键值对不存在时,才会进行设置,否则不做赋值操作
  • SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间

执行下面的命令时,只有 key 不存在时,SET 才会创建 key,并对 key 进行赋值。另外,key 的存活时间由 seconds 或者 milliseconds 选项值来决定

1
SET key value [EX seconds | PX milliseconds] [NX]
1
2
// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000
  • unique_value 是客户端的唯一标识,可以用一个随机生成的字符串来表示
  • PX 10000 则表示 lock_key 会在 10s 后过期

因为在加锁操作中,每个客户端都使用了一个唯一标识,所以在释放锁操作时,我们需要判断锁变量的值,是否等于执行释放锁操作的客户端的唯一标识

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

在释放锁操作中,使用了 Lua 脚本,这是因为,释放锁操作的逻辑也包含了读取锁变量、判断值、删除锁变量的多个操作,而 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

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

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

Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

Redlock 算法的执行步骤

  • 第一步是,客户端获取当前时间
  • 第二步是,客户端按顺序依次向 N 个 Redis 实例执行加锁操作
  • 第三步是,一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时
    • 客户端只有在满足下面的这两个条件时,才能认为是加锁成功
      • 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁
      • 条件二:客户端获取锁的总耗时没有超过锁的有效时间
      • 在满足了这两个条件后,需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
      • 如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作

在 Redlock 算法中,释放锁的操作和在单实例上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。这样一来,只要 N 个 Redis 实例中的半数以上实例能正常工作,就能保证分布式锁的正常工作了。

在实际的业务应用中,如果你想要提升分布式锁的可靠性,就可以通过 Redlock 算法来实现。

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus