编辑
2023-11-04
缓存中间件
0
请注意,本文编写于 182 天前,最后修改于 33 天前,其中某些信息可能已经过时。

目录

有哪些方案?
说说红锁(RedLock)
缓存续期(看门狗机制)

有哪些方案?

1、setnx + expire

但是setnx和expire分2步执行,非原子操作;若setnx执行成功,但expire执行失败,就可能出现死锁。而且不支持阻塞等待、不可重入。还有一个问题,如果业务逻辑执行时间超过锁的过期时间,没有办法续约,其他线程可以抢到锁,导致并发问题。

2、Redisson实现分布式锁

说说红锁(RedLock)

RedLock算法

​ RedLock:基于Redis实现的分布式锁,在分布式环境下必须保证不同的进程是以互斥的方式使用共享资源,具体的实现根据编程语言有很多,Java中是Redisson。

加锁:加锁实际上就是redis中,给key设置一个值,为避免死锁,还需要给定一个超时时间。

解锁:将key删除,但是前提是只能自己删除自己的锁,而且需要保证删锁的原子性。

超时:key要有过期时间,不能长时间占用。

​ Redis加锁只作用在一个Redis节点上,无论通过哨兵或者主从保证高可用,当master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

​ 1、客户端1在Redis的master节点上拿到了锁

​ 2、Master宕机了,存储锁的key还没有来得及同步到Slave上

​ 3、master故障,发生故障转移,slave节点升级为master节点

​ 4、客户端2从新的Master获取到了对应同一个资源的锁

 于是,客户端1和客户端2同时持有了同一个资源的锁,锁的安全性被打破了,RedLock正是用来解决这个问题的方案:

​ 客户端按照下面的步骤来获取锁:

​ 1、获取当前时间的毫秒数T1。

​ 2、按顺序依次向N个Redis节点执行获取锁的操作。这个获取锁的操作基于单Redis节点获取锁的过程相同。包括唯一UUID作为Value以及锁的过期时间(expireTime)。为了保证在某个在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还需要一个超时时间。它应该远小于锁的过期时间。客户端向某个Redis节点获取锁失败后,应立即尝试下一个Redis节点。这里失败包括Redis节点不可用或者该Redis节点上的锁已经被其他客户端持有。

​ 3、计算整个获取锁过程的总耗时。即当前时间减去第一步记录的时间。计算公式为T2 = now() - T1。如果客户端从大多数Redis节点( > N/2 + 1)成功获取到锁。并且获取锁总共消耗的时间小于锁的过期时间(即T2 < expireTime)。则认为客户端获取锁成功,否则,认为获取锁失败。

​ 4、如果获取锁成功,需要重新计算锁的过期时间。它等于最初锁的有效时间减去第三步计算出来获取锁消耗的时间,即expireTime - T2。

​ 5、如果最终获取锁失败,那么客户端立即向所有Redis发起释放锁的操作。

​ 虽然说RedLock算法可以解决单点Redis分布式锁的安全性问题,但如果集群中有节点发生崩溃重启,还是会锁的安全性有影响的。具体出现问题的场景如下:

假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

​ 1、客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)

​ 2、节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了

​ 3、节点C重启后,客户端2锁住了C, D, E,获取锁成功

  这样,客户端1和客户端2同时获得了锁(针对同一资源)。针对这样场景,解决方式也很简单,也就是让Redis崩溃后延迟重启,并且这个延迟时间大于锁的过期时间就好。这样等节点重启后,所有节点上的锁都已经失效了。也不存在以上出现2个客户端获取同一个资源的情况了。 

  相比之下,RedLock安全性和稳定性要好很多,但要说完全没有问题那也不是。例如,如果客户端获取锁成功后,如果访问共享资源操作执行时间过长,导致锁过期了,后续客户端获取锁成功了,这样在同一个时刻又出现了2个客户端获得了锁的情况。所以针对分布式锁的应用的时候需要多测试。服务器台数越多,出现不可预期的情况也越多。如果客户端获取锁之后,在上面第三步发生了GC得情况导致GC完成后,锁失效了,这样同时也使得同一时间有2个客户端获得了锁。如果系统对共享资源有非常严格要求得情况下,还是建议需要做数据库锁得得方案来补充。如飞机票或火车票座位得情况。对于一些抢购获取,针对偶尔出现超卖,后续可以人为沟通置换得方式采用分布式锁得方式没什么问题。因为可以绝大部分保证分布式锁的安全性。

缓存续期(看门狗机制)

​ 额外起一个线程,定期检查线程是否还持有锁,如果有延长过期时间。Redisson里面实现了这个方案,使用看门狗定期检查(每1/3的锁时间检查一次),若果线程还持有锁,则刷新过期时间。

看门狗续期流程

​ 通过exists判断,如果所存在,则设置值和过期时间,加锁成功。

​ 通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功。

​ 如果所以存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间,加锁失败。

​ 加锁成功后,在redis的内存数据中,就有一条hash结构的数据,key为锁的名称,field为随机字符串+线程ID,值为1。如果同一个线程多次调用lock方法,值递增1。

本文作者:whitebear

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!