理论结合实践:

这是一个使用 redis 作为分布式锁的一个 demo 工具(没人敢用的那种),读完下面的内容也许你会好奇阻塞是如何实现的,demo 代码中使用了一种 redis 本身的一种阻塞方式(这也许对你所知的 redis 单线程是一个不小的冲击,依然是单线程不过你得好好理解一下了)。

GitHub - Kicey/redis_lock: use redis key as distributed lock
use redis key as distributed lock. Contribute to Kicey/redis_lock development by creating an account on GitHub.

redis 实现分布式锁主要有两种方式:

setnx

setnx 是一个 redis 指令(SET IF NOT EXISTS),如果 key 不存在,那么设置成功并返回 1,否则不进行设置并返回 0。

由于 redis 本身的特性(单线程),受到的 redis 指令是串行执行的,不存在争用的问题,setnx 的 key 值就可以作为一个节点间的互斥信号量(值只能为 1)。

仅仅使用 setnx 存在锁在拥有锁的节点宕机之后不能得到释放,需要人为的干预。于是需要为锁设置一个过期时间。

设置这个过期时间有很多方式:

  1. 使用 key-value 对中的 value 代表锁的过期时间
  2. 使用 lua 脚本让 redis 将两个指令合并执行
  3. SET key value[EX seconds][PX milliseconds][NX|XX],redis 本身提供的一种在设置 key-value 时先进行检查并设置过期时间的指令

在上面的几种方式中,我们都假定获取了锁的节点在临界区结束时释放锁(删除 key-value),如果持有锁的节点宕机,那么需要等到锁过期(为了不会在临界区执行时就将锁释放掉,锁的过期时间一般是预估时间的数倍,可能达到几分钟)。这段时间内其他的节点对锁的请求将被阻塞。

这三种方式解决了锁的持有节点宕机导致锁无法释放的问题。还存在一个问题:如果在当前节点释放锁之前,锁就已经过期并被其他节点持有,当前节点释放锁的操作将影响另外一个节点。

为了解决这个问题,我们需要将锁的持有者信息写入 key-value 中,这样不管有没有持有锁,任何一个节点都知道当前持有锁的是哪一个节点,只有当节点属于自己时才会释放锁。

红锁

红锁是为了在 redis 集群中使用 redis 锁的一种机制。

步骤如下:

  1. 获取当前时间,以毫秒为单位。
  2. 按顺序向 5 个 master 节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为 10 秒,则超时时间一般在 5 - 50 毫秒之间,我们就假设超时时间是 50ms 吧)。如果超时,跳过该 master 节点,尽快去尝试下一个master节点。
  3. 客户端使用当前时间减去开始获取锁时间(即步骤 1 记录的时间),得到获取锁使用的时间。当且仅当超过一半( N/2+1,这里是 5/2+1=3 个节点)的 Redis master 节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s > 30ms + 40ms + 50ms + 4m0s + 50ms)。
  4. 如果取到了锁,key 的真正有效时间是第一个获取的 key 的有效时间减去获取锁所使用的时间。
  5. 如果获取锁失败,客户端要在所有的 master 节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,这里需要像上面一样判断当前锁的持有者是否为当前节点)。
参考:七种方案!探讨Redis分布式锁的正确使用姿势 - 掘金 (juejin.cn)