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 脚本来实现。

除了轮询,还可以通过 redispub-sub 模式订阅锁的释放信息。

3.单点问题

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

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

Redlock实现方案

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

不过,是不是这样就完美了呢?当然不是。

1. 重启问题

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

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

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

2. 加锁失败

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

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

参考

Magic Kaito - 深度剖析:Redis分布式锁到底安全吗

Last updated