我们都知道可以使用Redis实现一个分布式锁。
加锁时:
1 | // 一条命令保证原子性执行 |
解锁时,使用lua脚本保证原子性:
1 | // 判断锁是自己的,才释放 |
但是这样的方案解决不了锁过期的问题,也就是说如果加锁的进程在解锁之前,锁已经到了过期时间的话,锁就会被释放,从而其他进程就可以获取到锁。分布式锁的互斥性就被打破了。
解决这个问题的思路就是续租。加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。这个方案已经有了好的实现,Redisson。
上述的解决方案在单实例Redis中可以很好地发挥作用,如果使用的是Redis集群的话,就会有问题。比如由于Redis的复制是异步的,当master上的锁还没有来的及同步到其他副本上,此时发生主从切换,就会出现锁丢失的情况。
针对这种情况,Redis的作者提出了Redlock
方案。
Redlock
在Redis的分布式环境中,我们假设有N个Redis Master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。
为了取到锁,客户端应该执行以下操作:
- 获取当前时间戳T1,以毫秒为单位。
- 依次尝试从N个Master实例使用相同的key和随机值获取锁。当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
- 客户端使用当前时间T2-T1就得到获取锁使用的时间。当且仅当从大多数的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
同样地,Redisson也提供了实现。
1 | Config config1 = new Config(); |
总结
可以看到,Redlock
是依赖各个Redis的时钟保持一致。所以这一方案提出后也受到了很多人的质疑,Redis的作者也做出了相应的回应。具体过程很复杂,可以参考参考链接的内容。
除了Redis之外,我们还可以使用Zookeeper实现分布式锁,它主要依靠临时节点断开连接自动删除的特性以及watch机制实现。但是性能上还不如Redis。
在我们正常的部署场景中,一般只需要一个Redis Cluster,或者一个主从复制 + Sentinel。而这两者都不能承载RedLock的落地,RedLock方案需要专门搭建一套环境。所以,如果不是对分布式锁可靠性有极高的要求(比如金融场景),不太建议使用RedLock方案。
所以大部分场景,我都会使用单机版 Redis实现分布式锁。