浅析Redis分布式锁的实现方法
浅析Redis分布式锁的实现方法
背景
在分布式系统的开发中,我们往往需要解决多进程间的并发安全性问题,此时就需要实现一个分布式锁。
想要解决分布式系统的锁问题,我们就需要让锁 不单依赖于某个系统,需要将锁 给提取出来,独立的为所有系统服务。
在一个分布式系统中,缓存往往是必不可少的,故而我们常用的Redis就很好的接过了这个任务!
SETNX + EXPIRE
在Key-Value形式的Redis数据结构中,Key具有唯一性,我就可以用一个Key来唯一标识一个锁,
当一个服务需要加锁时,就在Redis中尝试获取这个锁的唯一Key,如果这个Key存在(获取到了Key),就代表锁已经被其它服务持有,加锁失败。
如果这个Key不存在(没有获取到Key),就代表锁还未被持有,该服务就可以在Redis中设置一个Key,代表已经持有锁,成功加上锁了。
其中,尝试获取锁的Key,若Key已存在,则加锁失败,否则加锁成功 的流程,在Redis中提供了一个命令:SETNX,可以一步完成:
SETNX在设置Key之前会先判断Key是否已经存在,若已存在则返回设置失败,否则就成功设置Key。
如果要释放锁,直接删除这个Key即可。
此时,我们已经利用Redis的唯一Key性质简单实现了分布式锁!
但是!
当服务成功加上锁后,必须要服务在Redis中删除这个锁才能释放掉锁,让其它服务继续获取锁。
如果服务在成功加上锁后,在释放锁之前宕机了,或者释放锁失败了!锁就永远存在Redis中了,其它服务永远也获取不到锁了!
其实在Redis中,我们可以通过EXPIRE命令,给某个Key加上过期时间,达到过时间,Key就会自动删除,也就是锁会自动释放。这样就解决了上面的问题。
原子性
但是!
SETNX与EXPIRE命令是两个命令,它们在一起不具备原子性 ,也就是说:
SETNX加锁成功后,EXPIRE给锁设置过期时间可能会失败,这样还是会产生上面的问题!
可重入性
其实还有一个问题:
每个服务都可以访问Redis,那么,就有可能把其他服务加的锁,给删除了!
此时,我们可以在设置锁的Key时,加入一个唯一标识,用以标识这个锁是哪个服务持有得到,
当其它在操作这个锁之前,要先比较一下唯一标识是否是自己的,如果不是,就不能操作,
否则,就知道这个锁是自己的,可以操作。
但是,这个方法“防君子,不防小人”,其它服务还是可以不管不顾,直接把锁给删了!
此外,如果想要给锁提供可重入性,
可以在锁的Key中设置一个整数,该数用以表明锁被重入的次数,当释放锁时,先对该数值减一,如果还是大于零,就代表还有本服务的其它线程持有该锁,还不能直接删除。
这样就简单实现了锁的重入,但是可用性较差。
发布订阅
此外,我们还可以利用上Redis的发布订阅功能:
当一个服务成功加上锁后,会发布一个主题,其它加锁失败的服务,会订阅这个主题。
当服务释放锁后,会在主题上广播这个信息,其它服务收到信息后就可以重新尝试获取锁了!
自动续约
此外,当服务成功获取锁,设置好过期时间后,如果程序执行时间太长,锁要失效了怎么办?
如果锁失效了,而程序还没有执行完,其它服务获取到锁后也来执行,就会出现并发问题。
我们除了给锁设置一个合理的过期时间外,还可以提供一个自动续约的机制:
设置合适的周期(要小于锁的过期时间),循环检测程序是否执行完成,若没有执行完成,则给锁刷新过期时间。
RedLock
此外,如果Redis崩了,分布式锁就无法服务了,我们就需要部署Redis集群以提高可用性。
对于Redis集群,加锁的时候,是单节点加锁,还是部分节点加锁,还是全部节点加锁?
如果是单节点加锁,加锁的节点如果宕机了,锁的信息就会丢失了。
如果是全部节点加锁,效率太低,绝大多数情况下,大部分节点是不会同时宕机的,所以只需要在大部分节点上加锁就行了,也就是部分节点加锁。
RedLock选择在N/2+1个节点加锁,只有在N/2+1个节点上都加锁成功了,才算最终加锁成功。
在RedLock下,服务需要依次尝试在集群中的节点上加锁,需要设置一个尝试超时时间,超时还未加锁成功的话,就换下一个节点尝试加锁,直到N/2+1个节点加锁成功,并且,加锁耗时小于锁的超时时间(也就是尝试一圈加完锁后,锁还没有过期),才算最终加锁成功。
如果加锁耗时超过锁的超时时间,必须要主动释放所有已经加上的节点锁。
Lua
在Redis中可以使用Lua脚本,这是一个原子性且高效的操作,
通过使用Lua脚本来设置Key和过期时间,可以解决SETNX与EXPIRE非原子性的问题。
Redisson
上面提到的所有技术方案,如果都要让我们自己来实现,未免有些太过麻烦!
Redisson为我们提供了更高效,更高可用的实现,我们可以直接使用:
结合Lua可以原子性的加锁和设置过期时间;
通过给Key中添加服务的唯一标识,以及设置整数值,实现锁的可重入性;
通过看门狗机制,定期检测程序如果没有执行完,就自动续约;
结合Redis的发布订阅机制来唤醒其它服务重试获取锁;
结合RedLock算法,提供RedissonRedLock解决单点失败问题。