redis实现分布式锁
原理
基于redis命令setnx key value
来实现分布式锁的功能,只有当key不存在时,setnx才可以设置成功并返回1,否则设置失败返回0。
方案1:
方案1存在的问题
假如在加锁成功,释放锁之前,服务器宕机了,这个锁就无法释放了,其他线程就永远无法获取到锁了。
改进之后
方案2
方案2存在的问题
获取锁和设置过期时间是2个命令,两次网络IO和redis交互,不具备原子性,可能出现获取锁成功之后 设置过期时间之前服务器宕机,导致其他线程永远都获取不到锁的现象。
方案3
1、将获取锁和设置过期时间放在lua脚本里面执行,lua脚本可以保证多个命令原子性的执行。
2、redis提供了其他命令在setnx的同时设置过期时间,分别如下所示:
- set key value PX 多少毫秒 NX
- set key value EX 多少秒 NX
方案3存在的问题
锁过期释放了,但是业务仍没有执行完,等到执行完业务并释放锁的时候,释放的其实是其他线程获取到的锁。
- 线程A获取到了锁,并设置过期时间10s。
- 当过了10秒后,线程A仍然没有执行完,但是此时线程A获取到的锁已经达到了过期时间,导致锁被释放了。
- 此时线程B获取到了锁,并执行业务方法
- 线程A业务方法执行完,并执行释放锁逻辑(此时释放的是线程B获取到的锁)
- 由于锁被释放,线程C也获取到了锁,并执行业务,此时线程B、线程C都获取到了同一个锁,并执行相应的业务。
方案4
方案4存在的问题
方案4中 判断key1的value1是否等于设置的值uuid,如果是则删除,否则不执行删除逻辑,由于是2个命令,不具备原子性导致可能出现以下场景
- 线程A成功获取锁key1,设置key1的value为uuid1,并设置过期时间10秒。
- 线程A执行完业务逻辑,准备删除锁
- 线程A获取到key1的value uuid1。
- 此时线程A的锁key1刚好到了过期时间
- cpu时间片切换,线程A停止执行,线程B开始执行
- 线程B尝试获取分布式锁key1(由于上面线程A设置的锁已经到了过期时间,所以此处可以获取成功)
- cpu时间片切换,线程A继续执行,由于锁的value == uuid1,所以开始删除锁(此时删除的是线程B设置的分布式锁)。导致出现了删除其他线程设置的分布式锁。
方案五
方案5存在的问题
当前仍存在锁时间达到了过期时间,但是线程没执行完的问题。
如何解决呢?
watch dog机制(Redisson底层就是基于watch dog机制来实现分布式锁的自动续期,当未设置过期时间时,会默认设置30秒,并起一个看门狗线程,每十秒去检测一次,如果当前线程仍然在执行,就自动续期,当然,如果手动设置了过期时间,就不会自动续期了)。
方案五存在的问题
redis集群的场景下,redis-master节点设置成功了,但是redis-slave节点未设置成功,如下所示:
- 当前redis集群有三个节点,分别是redis-master、redis-slave-a、redis-slave-b
- 线程A尝试获取分布式锁,此时在redis-master上设置key1成功。
- redis集群采用异步主从复制,但是redis-master上设置的key内容还没有同步到slave时,redis-master节点就宕机了。
- 此时集群重新选举,因此redis-slave-a变成了mater节点
- 此时有线程B尝试获取分布式锁key1,由于新的master节点上没有设置key1,因此线程B设置锁成功。
- 当前线程A和线程B就都持有了分布式锁并执行业务逻辑。
方案6
如何解决方案五仍然存在的问题呢?
使用redlock(红锁)算法,这是一个专门用于解决这种问题的分布式锁协议。
redlock:不能只在一个redis实例上加锁,应该是在多个redis实例上创建锁,当创建成功数量达到(n/2 + 1)时,才能认为加锁成功
以上方式实现复杂、性能差、运维繁琐。
我们的redis是AP高可用思想、如果要实现强一致性,应该使用CP思想的zookeeper来实现,解决主从一致性问题。
C:Consistency;一致性
A:Availabilit;可用性
P:Partition Tolerance;分区容错性