主从复制主节点宕机导致锁失效问题
主节点会不断的把自己的数据传递给从节点,来保证主节点的数据和从节点的数据是相等的, 毕竟不是在同一台机器,主和从之间会存在一定的延时,主从同步也会存在一定的延时
1)现在有一个线程1来进行从主节点上来获取锁的操作:set lock thread1 nx ex 10
2)然后主节点就会保存lock:thread1这把锁
3)然后主节点就会将这把锁的信息向同节点进行同步,但是就在此时主节点发生了故障,主从同步还没有完成主节点就宕机了,我们的redis中的哨兵会监视主从节点之间的工作状态,也就是监视集群的工作状态,如果主节点宕机了,那么首先客户端连接会断开,然后会从两个从节点中选出一个机器作为主节点;
4)然后哨兵模式随机选择一个从节点来充当主节点,但是刚才的主从同步操作没有完成就会导致所失效的问题,如果这个时候在有其他线程来尝试获取到锁,就会获取成功,从而产生线程安全问题;
解决方案:不区分主从节点,就算redis宕机了,也不会导致线程安全问题
加锁:每一个节点都保存了锁的信息才算加锁成功
@Configuration public class RedissonConfig { @Bean(name="redisson1") public RedissonClient GetRedissonConfig(){ //配置 Config config=new Config(); config.useSingleServer().setAddress("redis://124.71.136.248:6379").setPassword("12503487eA!"); //创建RedissonCliment对象 return Redisson.create(config); } @Bean(name="redisson2") public RedissonClient GetRedissonConfig2(){ //配置 Config config=new Config(); config.useSingleServer().setAddress("redis://124.71.136.248:6380").setPassword("12503487eA!"); //创建RedissonCliment对象 return Redisson.create(config); } @Bean(name="redisson3") public RedissonClient GetRedissonConfig3(){ //配置 Config config=new Config(); config.useSingleServer().setAddress("redis://124.71.136.248:6381").setPassword("12503487eA!"); //创建RedissonCliment对象 return Redisson.create(config); } }
现在我们来查看一下RedissonMultiLock的tryLock的源码:
1)waitTime:获取到锁失败之后在此时间内会一直尝试获取到锁
2)leaseTime:过了多长时间,如果该线程还没有解锁,那么这把锁会释放
如果对应的时间传入的是-1,代表你没有传递这个释放时间,如果这个值是-1,那么代表你没有传递这个过期时间,如果你不进行传递waittime的话,说明你压根就不想重试,在获取到锁失败之后就直接返回了,因为你压根没有给他重新尝试获取锁的时间
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException { long newLeaseTime = -1L; if (leaseTime != -1L) { if (waitTime == -1L) { //1.在你没有传递waitTime,此时你没有传递waittime说明你并不想重试,所以你的释放时间是多久,我就是用多久 newLeaseTime = unit.toMillis(leaseTime); } else { //2.如果你传递了waittime,说明你想要进行重试,不会用你释放的时间,那么锁过期的时间就是你等待的时间*2,因重新进行尝试可能耗时比较久,就防止万一重试时间小于等待时间,我还没有重试完呢,你就把锁释放了 newLeaseTime = unit.toMillis(waitTime) * 2L; } } //3.获取当前时间 long time = System.currentTimeMillis(); //4.获取到剩余时间,初始化,remaintime就是剩余的等待时间 long remainTime = -1L; if (waitTime != -1L) { remainTime = unit.toMillis(waitTime); } //5.这是计算锁的等待时间和剩余等待时间是相同的 long lockWaitTime = this.calcLockWaitTime(remainTime); //6.failedlocksLimit是剩余失败的锁的限制,在源码里面默认是0 int failedLocksLimit = this.failedLocksLimit(); //7. acquiredLocks代表已经获取到的锁的个数,表示加锁成功的个数 List<RLock> acquiredLocks = new ArrayList(this.locks.size()); //8.遍历到三个独立的锁 ListIterator iterator = this.locks.listIterator(); while(iterator.hasNext()) { RLock lock = (RLock)iterator.next(); boolean lockAcquired; try { if (waitTime == -1L && leaseTime == -1L) { //如果你不进行传递waittime表示只是进行重新尝试一次 lockAcquired = lock.tryLock(); } else { //表示你想进行尝试多次lockAcquired代表返回值,表示获取锁成功还是获取锁失败 long awaitTime = Math.min(lockWaitTime, remainTime); lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS); } } catch (RedisResponseTimeoutException var21) { this.unlockInner(Arrays.asList(lock)); lockAcquired = false; } catch (Exception var22) { lockAcquired = false; } if (lockAcquired) { //1.如果获取锁成功了,那么把它加入到acquiredLocks集合中,表示获取锁成功的集合 acquiredLocks.add(lock); } else { //2.当获取到某一把锁的时候获取失败了,判断锁得总的数量-获取到锁成功的数量==0,失败的上限 failedLocksLimit(),也就是说只有把所有的锁都获取到了,这个条件才成立 if (this.locks.size() - acquiredLocks.size() == this.failedLocksLimit()) { break; } //3.判断failedLocksLimit == 0==0 if (failedLocksLimit == 0) { //释放所有已经尝试获取成功的锁 this.unlockInner(acquiredLocks); if (waitTime == -1L) { //如果waittime为0,那么根本没有重试的机会,直接返回false return false; } //4代码走到这里面,说明程序是想要进行重试操作的,先把已经获取成功的锁进行清空 failedLocksLimit = this.failedLocksLimit(); acquiredLocks.clear(); //5.将迭代器向前进行遍历,再次尝试获取到锁 while(iterator.hasPrevious()) { iterator.previous(); } } else { --failedLocksLimit; } } if (remainTime != -1L) { //判断剩余的可以重新尝试获取锁的时间锁的时间 remainTime -= System.currentTimeMillis() - time; time = System.currentTimeMillis(); //如果剩余的可以重新尝试获取锁的时间已经小于0了,那么说明在之前尝试获取到锁的时候就已经超时了,那么程序就会让已经尝试获取到的成功的锁全部释放掉 if (remainTime <= 0L) { //释放所有已经曾经尝试获取成功的锁 this.unlockInner(acquiredLocks); return false; } } } //6.程序会进行判断当前锁的施放时间如果我们自己指定了 if (leaseTime != -1L) { List<RFuture<Boolean>> futures = new ArrayList(acquiredLocks.size()); Iterator var24 = acquiredLocks.iterator(); //7.如果程序已经成功地进入到这个循环,那么会使用迭代器来继续进行遍历,重新来给每一把锁expireAsync重新设置有效期,因为我们在尝试获取到每一把锁的时候,都会消耗一定的时间,如果程序是先进行获取到的锁1,那么锁1就立即开始进行倒计时的操作,此时程序还没来得及获取锁2呢,这显然是不符合逻辑的,只有说把三个锁都获取时间之后,把他们的时间进行重置,才能保证三把锁同时释放,防止有的锁释放了,有的锁没释放 //8.如果没有设置realsetime的时间,那么我们直接会触发看门狗机制,就永远不过期了 while(var24.hasNext()) { RLock rLock = (RLock)var24.next(); RFuture<Boolean> future = ((RedissonLock)rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS); futures.add(future); } var24 = futures.iterator(); while(var24.hasNext()) { RFuture<Boolean> rFuture = (RFuture)var24.next(); rFuture.syncUninterruptibly(); } } return true; }
分布式锁总结:
一)不可重入的redis分布式锁:
最早的锁,是利用setnx的互斥性,利用ex避免死锁,释放锁的时候判断线程标识避免误删
缺陷:是一个不可重入锁,无法进行重试,并且锁超时可能会有一些自动释放的风险
二)可重入的redis分布式锁:
原理:使用hash结构来记录线程标识和锁的重入次数,利用watchDong来自动延序锁的超时释放时间,确保不会因为业务执行时间过长而导致所自动释放,除非是服务器宕机,利用信号量控制锁重试等待,利用发布订阅这种方式来进行控制锁的等待时间,从而减少了锁重试的次数,避免浪费大量CPU资源,对于CPU利用率比较高
缺点:redis宕机导致所失效的问题,引起主从的一致性的问题
三)Redisson中的multiLock:
原理:多个独立的Redis节点,节点之间没有主从关系,就不会因为主从一致导致锁失效了,必须在所有节点都获取到这个可重入锁才算成功
缺点:至少建立三个独立节点,也可以给每一个独立节点建立多个主从
优惠卷优化: