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
成功还是失败,都应该执行一次删除操作。
参考
Last updated