> For the complete documentation index, see [llms.txt](https://wtifs.gitbook.io/diva-notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://wtifs.gitbook.io/diva-notes/redis/redis-fen-bu-shi-suo.md).

# Redis分布式锁

实现分布式锁有很多方案，例如基于数据库实现，基于 `zookeeper` 实现，如果吞吐量还是不能满足，比较广泛的做法是用分布式缓存来实现。

## Redis 单节点方式实现

核心就是围绕 `SETNX(SETIF NOT EXISTS)` 实现

当 `key` 不存在时返回 `1`，当 `key` 存在时返回 `0`。因为我们都知道 `redis` 是单线程的，所以在 `redis` 服务侧不会有线程安全问题。当返回 `1` 的时候认为获得锁成功，可以进行相应的业务处理，处理完成后，删除 `key`，释放锁，当返回 `0` 时表示失败

**1. 超时问题**

如果线程 `A` 拿到了锁，去处理业务的过程中，发生阻塞，例如数据库执行比较慢，或者服务发生故障了，这时候如果没有超时时间，系统将永久的死锁。所以 `setnx` 可以接受第三个参数，也就是超时时间。

这里面存在问题，因为你并不知道超时的情况下，业务到底有没有处理成功，还有没有在继续进行。所以，这里的锁并不是绝对的。

* 第一种解决方法就是靠程序员自己去把握，预估一下业务代码需要执行的时间，然后设置超时时间比执行时间长一些，保证不会因为自动解锁影响到客户端业务代码的执行。但是这并不是万全之策，比如网络抖动这种情况是无法预测的，也有可能业务代码执行的时间变长，所以并不安全。
* 有一种方法比较靠谱一点，就是给锁 **续期**。在 `Redisson` 框架实现分布式锁的思路，就使用 `watchDog` 机制实现锁的续期。当加锁成功后，同时开启守护线程，默认有效期是 `30` 秒，每隔 `10` 秒就会给锁续期到 `30` 秒，只要持有锁的客户端没有宕机，就能保证一直持有锁，直到业务代码执行完毕由客户端自己解锁，如果宕机了自然就在有效期失效后自动解锁。

**2. 如何释放锁？**

直接 `del` 可以吗？答案是否定的。

`service1` 删除自己的锁的时候，实际删掉的可能是锁超时后，`service2` 刚加的新锁，造成误删。

解决方案就是 `setnx` 的时候在客户端生成一个随机值作为 `value`。删除时判断 `value` 是否等于当前任务持有的随机值。当然，这个地方必须是原子的，否则，判断到删除之前还是有可能发生变化。可以通过 `lua` 脚本来实现。

除了轮询，还可以通过 `redis` 的 `pub-sub` 模式订阅锁的释放信息。

**3.单点问题**

还有另外一个问题，`redis` 是单点的，如果 `redis` 一旦挂掉，整个全玩完。

有人说，可以用 `master-slave` 啊，但是一样，如果 `master` 刚加锁、数据还未同步到从节点上就挂掉，则此时从节点升为主节点，但从节点上是没有锁的。

## Redlock实现方案

`Redlock` 是 `redis` 作者 `antirez` 大神在 `redis` 官网中给出的一种基于 `redis` 的分布式锁方案。直白点说，就是采用 `N`（通常是 `5`）个独立的 `redis` 节点，在全部节点上同时`setnx`，如果超过半数节点成功，就拿到了锁，这样就可以允许少数节点挂掉了。整个取锁、释放锁的操作和单节点类似。

不过，是不是这样就完美了呢？当然不是。

**1. 重启问题**

假设一共有5个节点（`A/B/C/D/E`），`service1` 成功获取了锁，注意这里，`service1` 在 `A/B/C` 上 `setnx` 成功，但是并没有在 `D/E` 上成功，如果 `C` 节点挂掉重启了，且 `C` 节点并没有持久化，这时候 `service2` 也可以成功锁住 `C/D/E`，导致锁失效。

有人说，那 `C` 持久化不就行了吗，实际上设置为同步的持久化方式对性能影响比较大。也就是常说的 `appendfsync always`，如果是机械硬盘，吞吐量可能会从 10万级降到几百。有人说用固态硬盘不就解决了吗？土豪是可以用的，吞吐量确实能到万级，但是大量的、频繁的写入容易导致写入放大。

这个问题的解决方案也非常简单。就是延迟重启，就是等到挂掉节点上的所有锁都过期了再重启，重启后，以前的锁都已经失效。（有些扯淡

**2. 加锁失败**

如果一个节点获取锁成功了，且获取详情为：四个节点都 `setnx` 成功，一个失败了，失败的情况是：发起请求成功，在返回 `ack` 的时候失败。这时候客户端会认为是失败了，而删除锁的时候没有删除这个节点，这就会导致过期之前，这个节点获取锁一直失败。

正确的做法是，解锁的时候，无论该节点之前 `setnx` 成功还是失败，都应该执行一次删除操作。

**参考**

[Magic Kaito - 深度剖析：Redis分布式锁到底安全吗](https://mp.weixin.qq.com/s/OpPxwu_WluMc0nnBHQRvdg)
