当前位置: 首页 > news >正文

Redisson分布式锁原理分析

1.Redisson实现分布式锁

在分布式系统中,涉及到多个实例对同一资源加锁的情况,传统的synchronized、ReentrantLock等单进程加锁的API就不再适用,此时就需要使用分布式锁来保证多服务之间加锁的安全性。
常见的分布式锁的实现方式有:zookeeper、Redis。Redis分布式锁相对简单,Redis分布式锁常用于业务场景中,Redisson是Redis实现分布式锁常用方式

1.1.Redisson锁使用

Redis分布式锁中,setnx命令,可保证一个key同时只能有一个线程设置成功,这样可实现加锁的互斥性。但是Redisson并未通过setnx命令实现加锁。

  1. 引入依赖
 <dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.15.6</version>
</dependency>
  1. 配置类
package com.hong.springbootjwt.config.redission;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.redisson.config.TransportMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author: hong* @date: 2023/2/16 20:09* @description RedissonClient*/
@Configuration
public class RedissonConfig {@Beanpublic RedissonClient redissonClient(){Config config = new Config();config.setTransportMode(TransportMode.NIO);SingleServerConfig singleServerConfig = config.useSingleServer();//可以用"rediss://"来启用SSL连接singleServerConfig.setAddress("redis://127.0.0.1:6379");singleServerConfig.setPassword("123456");return Redisson.create(config);}
}

Redisson加锁使用:

package com.hong.springbootjwt.service.impl;import com.hong.springbootjwt.service.UserService;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;/*** @author: hong* @date: 2023/2/16 20:08* @description*/
@Service
public class UserServiceImpl implements UserService {private final RedissonClient redissonClient;public UserServiceImpl(RedissonClient redissonClient) {this.redissonClient = redissonClient;}@Overridepublic void redissionLock() {// 获取锁对象RLock myLock = redissonClient.getLock("myLock");try {// 加锁myLock.lock();...}catch (Exception e) {}finally {// 释放锁myLock.unlock();}}
}

1.1.1.Redisson加锁原理

  1. 通过RedissonClient,传入锁的名称,获取到RLock(获得RLock接口的实现是RedissonLock),然后通过RLock实现加锁和释放锁
public RLock getLock(String name) {return new RedissonLock(this.commandExecutor, name);
}
  1. RedissonLock对lock()方法的实现
public void lock() {try {this.lock(-1L, (TimeUnit)null, false);} catch (InterruptedException var2) {throw new IllegalStateException();}
}
  1. 重载lock方法,传入leaseTime为-1,之后调用tryAcquire实现加锁
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {long threadId = Thread.currentThread().getId();Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);...
}
  1. tryAcquire最后调用到tryAcquireAsync方法,传入leaseTime和当前加锁线程ID,tryAcquire和tryAcquireAsync的区别在于,tryAcquireAsync是异步执行,而tryAcquire是同步等待tryAcquireAsync的结果,也即异步转同步的过程
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture ttlRemainingFuture;if (leaseTime != -1L) {ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}...
}
  1. tryAcquireAsync方法会根据leaseTime是否为-1,判断使用哪个分支加锁,不论走哪个分支,最后都调用tryLockInnerAsync方法实现加锁,只是参数不同。此处leaseTime=-1,走下面分支
    虽然传入tryAcquireAsync的leaseTime是-1,但在调用tryLockInnerAsync方法传入的leaseTime参数是this.internalLockLeaseTime,也即默认的30s
  2. 进入到tryLockInnerAsync方法,最终加锁是通过一段LUA脚本来实现的,Redis在执行LUA脚本时,可保证加锁的原子性,所以Redisson实现加锁的原子性是依赖LUA脚本实现的。
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, command,"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil;" + " end; " +"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); return nil;" +" end;" +" return redis.call('pttl', KEYS[1]);",Collections.singletonList(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
}
  1. 最后,对于RedissonLock这个实现类来说,最终实现加锁的逻辑都是通过tryLockInnerAsync来实现

1.1.2.LUA脚本实现加锁

Redis通过执行LUA脚本实现加锁,保证加锁的原子性,分析上述LUA脚本

  • KEY[1]:加锁的名称,此处demo,就是myLock
  • ARGV[1]:锁的过期时间,不指定的话默认是30s
  • ARGV[2]:代表加锁的唯一标识,由UUID和线程ID组成,一个Redisson客户端一个UUID(代表唯一的客户端),所以由UUID和线程ID组成加锁的唯一标识,可理解为某个客户端的某个线程加锁

这些参数是如何传过去的呢?其实就是在这里

  • getName:获取锁的名称
  • leaseTime:传入的锁的过期时间,没指定默认是30s
  • getLockName:就是获取加锁的客户端线程的唯一标识。

这段LUA的加锁逻辑:

  1. 调用Redis的exists命令,判断加锁的key是否存在,如果不存在,进入if。(不存在的话,就是没有某个客户端的线程来加锁,第一次加锁肯定没有加锁)于是第一次if条件成立
  2. 接着调用Redis的hincrby命令,设置加锁的key和加锁的某个客户端的某线程,加锁次数设置为1。(加锁次数很重要,是实现可重入锁特性的关键数据),用hash数据结构保存。hincrby命令完成后形成如下数据结构:
myLock:{"b983c153-7421-469a-addb-44fb92259a1b:1":1
}
  1. 最后,调用Redis的pexpire命令,将锁的过期时间设置为30s

总结:第一个客户端线程加锁的逻辑还是挺简单的,就是判断有无加过锁,没有的话自己去加锁,设置加锁的key,保存加锁的线程和加锁次数,设置锁的过期时间为30s

问题:为何要设置加锁key的过期时间?
主要原因是为了防止死锁,当某客户端获取到锁,还未来得及释放锁,当客户端宕机了,或者释放失败了,一旦没设置过期时间,那么这个锁key会一直存在,当其他线程来加锁的话发生key已经被加锁了,那么其他线程会一直加锁失败,从而造成死锁问题。

1.2.延长加锁时间

在加锁过程中,没指定锁的过期时间,Redisson也会默认给锁设置30s的过期时间,来防止死锁
虽然设置默认过期时间可防止死锁,但若在30s内,任务还未结束,但是锁已经释放失效了,一旦其他线程加锁成功,就可能出现线程安全,数据错乱问题。所以Redisson针对这种未指定超时时间的加锁,实现了一个watchdog机制,即“看门狗机制”自动延长加锁时间
在客户端通过tryLockInnerAsync方法加锁后,如果没指定锁过期时间,那么客户端会起一个定时任务,来定时延长加锁时间,默认10s执行一次,所以watchdog的本质就是一个定时任务

最后定期执行一段LUA脚本,实现加锁时间的延长

脚本中参数解释(同加锁的参数):

  • KEYS[1]:锁的名称,此demo为“myLock”
  • ARGV[1]:就是锁的过期时间
  • ARGV[2]:代表了加锁的唯一标识,b983c153-7421-469a-addb-44fb92259a1b:1

这段LUA脚本意思是判断续约的线程和加锁的线程是否为同一个,若为同一个,将锁的过期时间延长30s,然后返回1,代表续约成功,不是的话就返回0,续约失败,下一次定时任务就不会执行

注意:因为有了看门狗机制,所以若没有设置过期时间,并且没有主动释放锁,那么这个锁就永远不会释放,因为定时任务会不断延长锁的过期时间,造成死锁问题。
但是如果发生宕机,是不会造成死锁的,因为宕机了,服务也就没有了,那么看门狗的定时任务就没了,自然不会续约,等锁自动过期了,也就自动释放锁了。

1.3.实现可重入锁

可重入锁的意思就是,同一个客户端同一个线程多次对同一个锁进行加锁。
在Redisson中,可以执行多次lock方法,流程都是一样的,最后调用到LUA脚本,所以可重入锁的逻辑也是通过加锁的LUA脚本实现
下半部分就是可重入锁的逻辑

下面这段if的意思是:判断当前已经加锁的key对应的加锁线程,跟要加锁的线程是否为同一个,如果是,则将该线程对应的加锁次数加1,也即实现了可重入加锁,同时返回nil
可重入锁加锁成功后,加锁key和对应值可能是这样:

myLock:{"b983c153-7421-469a-addb-44fb92259a1b:1":2
}

1.4.主动释放锁

当业务执行完毕后,需要主动释放锁,为什么需要主动释放锁呢?

  1. 当任务执行完,未手动释放锁,如果没有指定锁的超时时间,那么因为看门狗机制,会导致这个锁无法释放,可能造成死锁问题
  2. 如果指定了超时时间,虽然不会造成死锁问题,但会造成资源浪费。假设设置超时时间为30s,但任务只执行了2s就完成,那么这个锁会还会被占用28s,这28s内其他线程就无法成功加锁。

Redisson如何主动释放锁以及避免其他线程释放自己加的锁呢?
主动释放锁是通过unlock方法实现,分析unlock方法的实现:

  1. unlock调用unlockAsync()方法,传入当前释放线程的ID,代表当前线程来释放锁(unlock其实也是将unlockAsync的异步操作转为同步操作)
public RFuture<Void> unlockAsync(long threadId) {RPromise<Void> result = new RedissonPromise();RFuture<Boolean> future = this.unlockInnerAsync(threadId);future.onComplete((opStatus, e) -> {this.cancelExpirationRenewal(threadId);if (e != null) {result.tryFailure(e);} else if (opStatus == null) {IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + threadId);result.tryFailure(cause);} else {result.trySuccess((Object)null);}});return result;
}
  1. unlockAsync最后会调用RedissonLock的unlockInnerAsync()实现释放锁的逻辑(也是执行一段LUA脚本)
protected RFuture<Boolean> unlockInnerAsync(long threadId) {return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);" +"if (counter > 0) then " +"redis.call('pexpire', KEYS[1], ARGV[2]);" +"return 0;" +"else redis.call('del', KEYS[1]);" +"redis.call('publish', KEYS[2], ARGV[1]);" +"return 1; " +"end; " +"return nil;", Arrays.asList(this.getRawName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)});
}

LUA脚本解释:

  1. 先判断来释放锁的线程是不是加锁的线程,如果不是,直接返回nil;可看出,此处是通过一个if条件防止线程释放了其他线程加的锁
  2. 如果释放锁的线程是加锁的线程,那么将锁次数减1,然后拿到加锁次数counter变量
  3. 若counter大于0,说明有重入锁,锁还未完全释放完,那么设置一下过期时间,然后返回0
  4. 若counter未大于0,说明此锁已经释放完成,将锁对应的key删除,然后发布一个锁已经释放的消息,然后返回1

1.5.超时自动释放锁

已知如果不指定超时时间的话,存在看门狗线程不断延长加锁时间,不会导致锁超时释放,自动过期,那么指定超时时间的话,是如何实现指定时间释放的呢?
能够设置超时时间的方法:

// 通过传入leaseTime参数可指定锁超时时间
void lock(long leaseTime, TimeUnit unit)boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)

已知,无论是否设置锁超时时间,最终都会调用tryAcquireAsync方法进行加锁。只是不指定超时时间的话,传入的leaseTime值是-1,也即不指定超时时间,但是Redisson默认还是会设置30s过期时间;当指定超时时间,那么leaseTime就是自己指定的时间,最终也是通过一个LUA脚本进行加锁逻辑
是否指定超时时间的区别:

  • 不指定超时时间,会开启watchdog后台线程,不断续约加锁时间
  • 指定超时时间,就不会开启watchdog定时任务,这样就不会续约,加锁key到了过期时间就会自动删除,即达到释放锁的目的


总结:
指定超时时间,达到超时释放锁的功能主要通过Redis自动过期来实现,因为指定了超时时间,加锁成功后就不会开启watchdog机制来延长加锁时间
实际项目中,若能比较准确预估代码执行时间,那么可以指定锁超时释放时间,来防止业务执行错误导致无法释放锁的问题;若不能预估代码执行时间,那么可以不指定超时时间,在finally代码块中采用unlock手动释放。

1.6.实现不同线程加锁的互斥

前面已经分析过,第一次加锁逻辑和可重入锁的逻辑,因为LUA脚本加锁的逻辑同时只有一个线程能够执行(Redis是单线程的原因),所以一旦有线程加锁成功,那么另一线程来加锁,前面两个if条件都不成立,最后通过调用Redis的pttl命令返回锁的剩余过期时间回去。
这样,客户端就可根据返回值判断是否加锁成功, 因为第一次加锁和可重入锁的返回值都是nil,而加锁失败就返回了锁的剩余过期时间

// 第一次加锁
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
// 可重入锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return nil;
end;
// 无法加锁成功
return redis.call('pttl', KEYS[1]);

所以加锁的LUA脚本通过条件判断就可实现加锁的互斥操作,保证其他线程无法加锁成功。

总结:加锁的LUA脚本实现了第一次加锁、可重入锁、加锁互斥的逻辑

1.7.加锁失败如何实现阻塞等待加锁

上面分析已知,加锁失败后,会走如下代码:

这里可看出,最终会执行死循环(自旋)的方式,不停的通过tryAcquire()方法来实现加锁,直到加锁成功后才会跳出死循环,如果一直没有加锁成功,那么就会一直旋转下去,所谓阻塞,就是自旋加锁的方式
但是这种阻塞可能产生问题,如果其他线程释放锁失败,那么这个阻塞加锁的线程会一直阻塞加锁,肯定会出问题的,所以需要设置超过一定时间还未加锁成功的话,就放弃加锁。

超时放弃加锁的方法:

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
boolean tryLock(long time, TimeUnit unit)

通过waitTime参数或time参数来指定超时时间,这两个方法的主要区别在于是否支持指定锁超时时间

do {long currentTime = System.currentTimeMillis();ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);if (ttl == null) {var16 = true;return var16;}// 超过尝试时间,加锁失败,返回falsetime -= System.currentTimeMillis() - currentTime;if (time <= 0L) {this.acquireFailed(waitTime, unit, threadId);var16 = false;return var16;}currentTime = System.currentTimeMillis();if (ttl >= 0L && ttl < time) {((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}time -= System.currentTimeMillis() - currentTime;
} while(time > 0L);

从源码可看出,实现一定时间内还未获取到锁,就放弃加锁的逻辑,其实相比于一直自旋获取锁,主要是加了超时判断,如果超时了,就退出循环,放弃加锁

1.8.实现公平锁

什么是公平锁?
公平锁就是指线程成功加锁的顺序,跟线程请求加锁的顺序一样,实现了先来先成功加锁的特点,不插队才叫公平
前面所说的RedissonLock的实现都是非公平锁,但里面有些机制如watchdog机制是公平的
公平锁和非公平锁比较
公平锁

  • 优点:按顺序平均分配锁资源,不会出现线程饿死(即某一线程长时间未获得锁)的情况
  • 缺点:按顺序唤醒线程的开销大,执行性能不高

非公平锁:

  • 优点:执行效率高,谁先获得锁,谁就先执行,无需按顺序唤醒
  • 缺点:资源分配随机性强,可能出现线程饿死的情况

如何使用公平锁
通过RedissonClient的getFairLock可获取到公平锁。Redisson对于公平锁的实现是RedissonFairLock类,通过RedissonFairLock来加锁,可实现公平锁的特性

public void redissionLock() {// 获取锁对象RLock myLock = redissonClient.getFairLock("myLock");try {// 加锁myLock.lock(30,TimeUnit.SECONDS);} catch (Exception e) {} finally {// 释放锁myLock.unlock();}
}

RedissonFairLock继承了RedissonLock,主要重写了tryLockInnerAsync方法,也就是加锁逻辑的方法。

概述这段LUA的作用:

  1. 当线程来加锁的时候,如果加锁失败,将线程放置到一个set中,这样就按照加锁顺序给线程排队,set集合的头部的线程就代表接下来要加锁成功的线程
  2. 当有线程释放锁之后,其他加锁失败的线程就会继续来实现加锁
  3. 加锁前判断一下set集合的头部线程跟当前要加锁的线程是否同一个
  4. 如果是同一个,那么加锁成功
  5. 如果不是的话,就加锁失败,这样就实现了加锁的顺序性

1.9.实现读写锁

在实际开发中,会有很多“读多写少”的场景,对于这种场景,使用独占锁加锁,在高并发情况下,会导致大量线程加锁失败,阻塞,对系统吞吐量有一定影响,为了适配这种“读多写少”的场景,Redisson也实现了读写锁的功能
读写锁的特点:

  • 读与读是共享的,不互斥
  • 读与写互斥
  • 写写互斥

Redisson中使用读写锁:

public void redissionLock() {// 获取读写锁RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("readWriteLock");RLock readLock = readWriteLock.readLock();try {readLock.lock();// 业务操作...} catch (Exception e) {} finally {// 释放锁readLock.unlock();}RLock writeLock = readWriteLock.writeLock();try {writeLock.lock();// 业务操作...} catch (Exception e) {} finally {// 释放锁writeLock.unlock();}
}

Redisson通过RedissonReadWriteLock类实现读写锁功能。通过这个类可以获取到读锁和写锁,所以真正的加锁逻辑是由读锁和写锁实现的
Redisson是如何具体实现读写锁的?
前面已知,加锁成功后会在Redis中维护一个hash的数据结构,存储加锁线程和加锁次数。在读写锁的实现中,会往hash数据结构中多维护一个mode字段,来表示当前加锁的模式。
所以能够实现读写锁,最主要是因为维护了一个加锁模式的字段mode,这样当线程来加锁的时候,就能根据当前加锁模式结合读写的特性来判断要不要让当前线程加锁成功

  • 若没有加锁,那么不论读锁还是写锁都能加锁成功,成功后根据加锁类型维护mode字段
  • 若模式是读锁,加锁线程也是加读锁的,就让它加锁成功
  • 若模式是读锁,加锁线程是加写锁的,就让它加锁失败
  • 若模式是写锁,不论线程是加写锁还是读锁,都让它加锁失败(加锁线程自己除外,可重入特性)

1.10.实现批量加锁(联锁)

批量加锁的意思是同时加几个锁,只有这些锁都加成功了,才算真正的加锁成功!
比如:一个下单业务中,同时需要锁定订单、库存、商品,基于这种需要锁多种资源的场景中,Redisson提供了批量加锁的实现,对应的实现类是RedissonMultiLock
使用联锁:

public void redissionLock() {// 获取读写锁RLock myLock1 = redissonClient.getLock("myLock1");RLock myLock2 = redissonClient.getLock("myLock2");RLock myLock3 = redissonClient.getLock("myLock3");RLock multiLock = redissonClient.getMultiLock(myLock1,myLock2,myLock3);try {multiLock.lock();// 业务操作...} catch (Exception e) {} finally {// 释放锁multiLock.unlock();}
}

Redisson对于批量加锁的实现也很简单,源码如下:

就是根据顺序依次调用tryLock,传入myLock1,myLock2,myLock3加锁方法,如果都成功加锁了,那么multiLock就算加锁成功

1.11.RedLock算法

对于单Redis实例来说,如果Redis宕机了,那么整个系统就无法运行,所以为了保证Redis的高可用,一般都会采用主从或哨兵模式,但是一旦使用了主从或哨兵模式,此时Redis的分布式锁就可能出现问题
例如,使用哨兵模式

基于这种模式,Redis客户端会在master节点上加锁,然后异步复制到slave节点上。但是一旦master节点宕机,那么哨兵感知到,就会从slave节点选择一个节点作为主节点。
假设客户端对原主节点加锁,加锁成功后还未来得及同步到从节点,主节点宕机了,从节点变为了主节点,此时从节点是没有加锁信息的,如果其他客户端来加锁,是能够加锁成功的!!
针对此问题,Redis官方提供一种RedLock算法,Redisson刚好实现了这种算法

RedLock算法
在Redis分布式环境中,假设有N个master节点,这些节点相互独立,不存在主从复制或其他集群协调机制。
前面描述过,在Redis单例下怎么安全获取和释放锁,需要确保将在N个实例上使用此方法获取和释放锁。为了获取锁,客户端应该执行以下操作:

  1. 获取当前Unix时间,以ms为单位
  2. 依次尝试从N个实例,使用相同key和随机值获取锁,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间小于锁的失效时间。这样可避免服务器端Redis已经挂掉情况下,客户端还在等待响应结果,如果服务器端没有在规定时间内响应,客户端应尽快尝试其他Redis实例
  3. 客户端使用当前时间减去开始获取锁的时间(步骤1记录的时间),就得到获取锁使用的时间,并且仅当从大多数(3个节点,共5个)的Redis节点中获取到锁,并且使用时间小于锁失效时间时,锁才算获取成功
  4. 如果获取到锁,key的真正有效时间等于有效时间减去获取锁所使用时间(步骤3计算所得结果)
  5. 若因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例上获取到锁,或取锁时间已经超过有效时间),客户端应该在所有Redis实例上进行解锁(即使某些Redis实例根本没有加锁成功)

Redisson对RedLock算法的实现:


RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

RedissonRedLock加锁过程如下:

  1. 获取所有Redisson Node节点信息,循环向所有Node节点加锁,假设节点数为N,一个Redisson Node代表一个主从节点
  2. 若在N个节点中,有 N/2 + 1个节点加锁成功,那么整个RedissonRedLock加锁成功
  3. 若在N个节点中,小于N/2 + 1个节点加锁成功,那么整个RedissonRedLock加锁失败
  4. 若中途发现各节点加锁总耗时,大于等于设置的最大等待时间,则直接返回失败

RedissonRedLock底层其实也是基于RedissonMultiLock实现的,RedissonMultiLock要求所有的加锁成功才算成功,RedissonRedLock要求只要有N/2+1个成功就算成功

相关文章:

Redisson分布式锁原理分析

1.Redisson实现分布式锁 在分布式系统中&#xff0c;涉及到多个实例对同一资源加锁的情况&#xff0c;传统的synchronized、ReentrantLock等单进程加锁的API就不再适用&#xff0c;此时就需要使用分布式锁来保证多服务之间加锁的安全性。 常见的分布式锁的实现方式有&#xff…...

【Linux】:线程(二)互斥

互斥与同步 一.线程的局部存储二.线程的分离三.互斥1.一些概念2.上锁3.锁的原理4.死锁 一.线程的局部存储 例子 可以看到全局变量是所有线程共享的&#xff0c;如果我们想要每个线程都单独访问g_val怎么办呢&#xff1f;其实我们可以在它前面加上__thread修饰。 这就相当于把g…...

vscode报错Pylance client: couldn‘t create connection to server.

问题描述&#xff1a; 一打开vscode&#xff0c;右下角就弹报错&#xff0c;Pylance client: couldn’t create connection to server.&#xff0c;让我打开output&#xff0c;打开后似乎是在说连不上server 因为连不上server&#xff0c;所以我的python代码没法解析&#xff0…...

智能优化算法应用:基于萤火虫算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于萤火虫算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于萤火虫算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.萤火虫算法4.实验参数设定5.算法结果6.参考文…...

MacOS多屏状态栏位置不固定,程序坞不小心跑到副屏

目录 方式一&#xff1a;通过系统设置方式二&#xff1a;鼠标切换 MacOS多屏状态栏位置不固定&#xff0c;程序坞不小心跑到副屏 方式一&#xff1a;通过系统设置 先切换到左边 再切换到底部 就能回到主屏了 方式二&#xff1a;鼠标切换 我的两个屏幕放置位置如下 鼠标在…...

Python:pipdeptree 语法介绍

相信大家在按照一些包的时候经常会碰到版本不兼容&#xff0c;但是又不知道版本之间的依赖关系&#xff0c;今天给大家介绍一个工具&#xff1a;pipdeptree pipdeptree 是一个 Python 包&#xff0c;用于查看已安装的 pip 包及其依赖关系。它以树形结构展示包之间的依赖关系&am…...

【问题处理】—— lombok 的 @Data 大小写区分不敏感

问题描述 今天在项目本地编译的时候&#xff0c;发现有个很奇怪的问题&#xff0c;一直提示某位置找不到符号&#xff0c; 但是实际在Idea中显示确实正常的&#xff0c;一开始以为又是IDEA的故障&#xff0c;所以重启了IDEA&#xff0c;并执行了mvn clean然后重新编译。但是问…...

跟着我学Python基础篇:08.集合和字典

往期文章 跟着我学Python基础篇&#xff1a;01.初露端倪 跟着我学Python基础篇&#xff1a;02.数字与字符串编程 跟着我学Python基础篇&#xff1a;03.选择结构 跟着我学Python基础篇&#xff1a;04.循环 跟着我学Python基础篇&#xff1a;05.函数 跟着我学Python基础篇&#…...

Tomcat部署(图片和HTML等)静态资源时遇到的问题

文章目录 Tomcat部署静态资源问题图中HTML代码启动Tomcat后先确认Tomcat是否启动成功 Tomcat部署静态资源问题 今天&#xff0c;有人突然跟我提到&#xff0c;使用nginx部署静态资源&#xff0c;如图片。可以直接通过url地址访问&#xff0c;为什么他的Tomcat不能通过这样的方…...

在接触新的游戏引擎的时候,如何能快速地熟悉并开发出一款新游戏?

引言 大家好&#xff0c;今天分享点个人经验。 有一定编程经验或者游戏开发经验的小伙伴&#xff0c;在接触新的游戏引擎的时候&#xff0c;如何能快速地熟悉并开发出一款新游戏&#xff1f; 利用现成开发框架。 1.什么是开发框架&#xff1f; 开发框架&#xff0c;顾名思…...

计网 - TCP四次挥手原理全曝光:深度解析与实战演示

文章目录 Pre导图过程分析抓包实战第一次挥手 【FIN ACK】第二次挥手 【ACK】第三次挥手 【FINACK】第四次挥手 【ACK】 小结 Pre 计网 - 传输层协议 TCP&#xff1a;TCP 为什么握手是 3 次、挥手是 4 次&#xff1f; 计网 - TCP三次握手原理全曝光&#xff1a;深度解析与实战…...

个人养老金知多少?

个人养老金政策你了解吗&#xff1f;税优政策你知道吗&#xff1f;你会计算能退多少税吗&#xff1f;… 点这里看一看...

gpt3、gpt2与gpt1区别

参考&#xff1a;深度学习&#xff1a;GPT1、GPT2、GPT-3_HanZee的博客-CSDN博客 Zero-shot Learning / One-shot Learning-CSDN博客 Zero-shot&#xff08;零次学习&#xff09;简介-CSDN博客 GPT1、GPT2、GPT3、InstructGPT-CSDN博客 目录 gpt2与gpt1区别&#xff1a; gp…...

PyQt6 QDateEdit日期控件

​锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计39条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话…...

【无线网络技术】——无线城域网(学习笔记)

&#x1f4d6; 前言&#xff1a;无线城域网&#xff08;WMAN&#xff09;是指在地域上覆盖城市及其郊区范围的分布节点之间传输信息的本地分配无线网络。能实现语音、数据、图像、多媒体、IP等多业务的接入服务。其覆盖范围的典型值为3~5km&#xff0c;点到点链路的覆盖可以高达…...

RK3568平台 OTA升级原理

一.前言 在迅速变化和发展的物联网市场&#xff0c;新的产品需求不断涌现&#xff0c;因此对于智能硬件设备的更新需求就变得空前高涨&#xff0c;设备不再像传统设备一样一经出售就不再变更。为了快速响应市场需求&#xff0c;一个技术变得极为重要&#xff0c;即OTA空中下载…...

mysql迁移步骤

MySQL迁移是指将MySQL数据库从一台服务器迁移到另一台服务器。这可能是因为您需要升级服务器、增加存储空间、提高性能或改变数据库架构。 以下是MySQL迁移的一般步骤&#xff1a; 以上是MySQL迁移的一般步骤&#xff0c;具体步骤可能因您的环境和需求而有所不同。在进行迁移之…...

计算机网络应用层(期末、考研)

计算机网络总复习链接&#x1f517; 目录 DNS域名服务器域名解析过程分类递归查询&#xff08;给根域名服务器造成的负载过大&#xff0c;实际中几乎不用&#xff09;迭代查询 域名缓存&#xff08;了解即可&#xff09;完整域名解析过程采用UDP服务 FTP控制连接与数据连接 电…...

Jenkins离线安装部署教程简记

前言 在上一篇文章基于Gitee实现Jenkins自动化部署SpringBoot项目中&#xff0c;我们了解了如何完成基于Jenkins实现自动化部署。 对于某些公司服务器来说&#xff0c;是不可以连接外网的&#xff0c;所以笔者专门整理了一篇文章总结一下&#xff0c;如何基于内网直接部署Jen…...

如果一个嵌套类需要在单个方法之外仍然是可见,或者它太长,不适合放在方法内部,就应该使用成员类。

当一个嵌套类需要在单个方法之外仍然是可见&#xff0c;或者它太长不适合放在方法内部时&#xff0c;可以考虑使用成员类&#xff08;成员内部类&#xff09;。成员类是声明在类的内部但不是在方法内部的类&#xff0c;可以访问外部类的实例成员。 以下是一个示例&#xff0c;…...

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…...

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

解锁数据库简洁之道:FastAPI与SQLModel实战指南

在构建现代Web应用程序时&#xff0c;与数据库的交互无疑是核心环节。虽然传统的数据库操作方式&#xff08;如直接编写SQL语句与psycopg2交互&#xff09;赋予了我们精细的控制权&#xff0c;但在面对日益复杂的业务逻辑和快速迭代的需求时&#xff0c;这种方式的开发效率和可…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)

可以使用Sqliteviz这个网站免费编写sql语句&#xff0c;它能够让用户直接在浏览器内练习SQL的语法&#xff0c;不需要安装任何软件。 链接如下&#xff1a; sqliteviz 注意&#xff1a; 在转写SQL语法时&#xff0c;关键字之间有一个特定的顺序&#xff0c;这个顺序会影响到…...

Python爬虫(一):爬虫伪装

一、网站防爬机制概述 在当今互联网环境中&#xff0c;具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类&#xff1a; 身份验证机制&#xff1a;直接将未经授权的爬虫阻挡在外反爬技术体系&#xff1a;通过各种技术手段增加爬虫获取数据的难度…...

【HTTP三个基础问题】

面试官您好&#xff01;HTTP是超文本传输协议&#xff0c;是互联网上客户端和服务器之间传输超文本数据&#xff08;比如文字、图片、音频、视频等&#xff09;的核心协议&#xff0c;当前互联网应用最广泛的版本是HTTP1.1&#xff0c;它基于经典的C/S模型&#xff0c;也就是客…...

【开发技术】.Net使用FFmpeg视频特定帧上绘制内容

目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法&#xff0c;当前调用一个医疗行业的AI识别算法后返回…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Yolov8 目标检测蒸馏学习记录

yolov8系列模型蒸馏基本流程&#xff0c;代码下载&#xff1a;这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中&#xff0c;**知识蒸馏&#xff08;Knowledge Distillation&#xff09;**被广泛应用&#xff0c;作为提升模型…...