这是一个比较典型的关于Redis分布式锁的综合性问题,其实考察的就是关于分布式锁的熟悉程度,尤其是Redis的分布式锁的熟悉程度。
我总结了一下关于这个问题可以回答的几个方向和关键点。
一个分布式锁有很多基本要求,比如说锁的互斥性、可重入性、锁的性能等问题。
对于锁的互斥性,可以借助setnx来保证,因为这个操作本身就是一个原子性操作,并且结合Redis的单线程的机制,就可以保证互斥性。
因为Redis是基于内存的,所以他的性能也是很高的,这个就没啥好说的了,大家都知道的。
至于可重入性,其实就是说一个线程,在锁没有释放的情况下,他是可以反复的拿到同一把锁的。并且需要在锁中记录加锁次数,用来保证重入几次就需要解锁几次。用setnx也是可以实现的。
当然,如果我们直接使用Redisson的话,他是支持可重入锁的实现的。可以直接用。
其实就是要确保只有锁的持有者能释放锁,避免其他客户端误解锁。这个问题其实挺傻的,但是我们实际就发生过,因为有的时候我们是在finally中去释放锁,finally有一定会执行,那么就可能会导致虽然没拿到锁,但是当他执行finally的时候,也可能把锁给解了。
所以,需要解决这个问题,我们就需要在使用setnx加锁时把具体持有锁的owner放进去,和上面一样,线程ID也好,业务单号也好,总之需要做一下判断。
如果用了Redisson是不存在这个问题的。
为了避免死锁,我们一般会给一个分布式锁设置一个超时时间,如上面我们用的setnx的方案,其实就是设置了一个超时时间的。
但是有的是,代码如果执行的比较慢的话,比如设置的超时时间是3秒,但是代码执行了5秒,那么就会导致在第三秒的时候,key超时了就自动解锁了,那么其他的线程就可以拿到锁了,这时候就会发生并发的问题了。
所以,我们需要有一个好的办法来解决。一种是设置一个更长的超时时间,避免提前释放,我见过有人把分布式锁设置半个小时。。。
但是这个方案非常不好,因为分布式锁是影响并发的,锁的时间长,意味着加锁时间段内只能有一个线程操作,那么并发度就会大大降低。(因为要考虑到解锁失败的问题)
还有一个好的办法,就是像redisson一样,实现一个watch dog的机制,给锁自动做续期,让锁不会提前释放。
但是需要注意的是,只有我们没有自己主动设置锁的超时时间的时候,watchdog才会续期,如果自己设置了超时时间,那么就不会给你续期了。具体看上面这个原理解读。
有了自动续期之后,锁就一定可靠了吗?其实也不是,这里会存在两个单点问题。
首先,在使用单节点Redis实现分布式锁时,如果这个Redis实例挂掉,那么所有使用这个实例的客户端都会出现无法获取锁的情况。
这个问题是有解的,就是引入集群模式,通过哨兵检测redis实例挂掉的情况,提升整个集群的可用性。
但是,这个方案同样存在一个单点故障带来的问题:
当使用集群模式部署的时候,如果master一个客户端在master节点加锁成功了,然后没来得及同步数据到其他节点上,他就挂了, 那么这时候如果选出一个新的节点,再有客户端来加锁的时候,就也能加锁成功,因为数据没来得及同步,新的master会认为这个key是不存在的。
为了解决这个问题,redis的作者提出了一个算法——RedLock,他通过这种算法来保证在半数以上加锁成功才认为成功,这样就可以确保即使master挂了,新选出来的master也会有之前的加锁数据。
具体原理见:
但是,引入红锁就万事大吉了么。也并不是。红锁同样存在问题。首先就是一个网络分区的问题。
在网络分区的情况下,比如集群发生了脑裂,不同的节点可能会获取到相同的锁,这会导致分布式系统的不一致性问题。
但是我们需要注意的是,这个情况虽然会存在节点获取到相同锁,但这种情况只会发生在网络分区发生时,且只会发生在一小部分节点上。而在网络分区恢复后,RedLock 会自动解锁。所以理论上来说是有这个风险,但是实际上来说发生的概率极低。
除了脑裂,还有一个时钟飘逸的问题,由于不同的机器之间的时间可能存在微小的漂移,这会导致锁的失效时间不一致,也会导致分布式系统的不一致性问题。
那么就会导致有的redis实例已经解锁了,那么就会使得新的客户端可以拿到锁。
这个问题的解决方案是,RedLock 可以使用 NTP 等工具来同步不同机器之间的时间,从而避免时间漂移导致的问题。