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

Redisson分布式锁原理解析

前言

首先Redis执行命令是单线程的,所以可以利用Redis实现分布式锁,而对于Redis单线程的问题,是其线程模型的问题,本篇重点是对目前流行的工具Redisson怎么去实现的分布式锁进行深入理解;开始之前,我们可以下你思考一个问题,Redisson的实现方式有何不同?为什么?

使用

引入依赖

 <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.25.0</version>
</dependency>

添加配置

    @Autowiredprivate RedisProperties redisProperties;@Beanpublic RedissonClient redisClient() {Config config = new Config();config.setTransportMode(TransportMode.NIO);SingleServerConfig singleServerConfig = config.useSingleServer();singleServerConfig.setAddress(String.format("redis://%s:%s", redisProperties.getHost(), redisProperties.getPort()));singleServerConfig.setPassword(redisProperties.getPassword());return Redisson.create(config);}

加锁

private final RedissonClient redissonClient;public void lock() throws Exception {RLock lock = redissonClient.getLock(PRODUCT_LOCK_KEY + id);if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {// 业务} else {// 业务}
}

如上使用是最简单的方式,Redission它底层封装了很多逻辑,但如果说要redis客户端实现,你要怎么实现?

Redis中本就有支持分布式锁的命令:setnx,对应RedisTemplate中,使用如下:

redisTemplate.opsForValue().setIfAbsent("lockkey", "", 1, TimeUnit.SECONDS);

使用Redis实现分布式锁需要注意的是不要产生死锁,所以使用Redis实现分布式锁有两种方式:

  1. setnx命令
  2. lua脚本

两种方式都是一个操作完成key,value的设置以及过期时间的设置,你是否有相关,他们的实现是否都一样,或者说,这个实现可以有其他实现方式?

源码

原理

  1. lua脚本保证多个命令的原子性;
  2. 采用hash数据结构,key为锁的名称,field是线程对应的名称,也因为这个数据结构,也支持可重入锁;
  3. 定时延时的操作避免死锁(看门狗);

lock()

我们先看lock()方法,tryLock()lock()底层是一样的,所以我们只看lock方法;

   @Overridepublic void lock() {try {lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();}}

位置:org.redisson.RedissonLock#lock(long, java.util.concurrent.TimeUnit, boolean)

传参是:(-1, null, false)

image-20240402175531643

/**
leaseTime: 过期时间
unit:过期时间单位
interruptibly: 信号量,是否打断线程
*/
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {// 取线程ID,后面作为锁的一个标志long threadId = Thread.currentThread().getId();// 加锁// 两个步骤:// 1. 利用lua脚本设置锁与过期时间(原子操作),过期时间lockWatchdogTimeout = 30 * 1000 = 30秒// 2. 执行过期时间刷新(这里的刷新时利用回调的方式 + 延迟执行实现的定时任务),lockWatchdogTimeout/3 = 10秒Long ttl = tryAcquire(-1, leaseTime, unit, threadId);// lua脚本中,加锁成功返回nil,对应redis中是null,加锁失败,则返回的是已存在的锁的过期时间// 所以这里返回null,就是加锁成功了,就不再往下走了if (ttl == null) {return;}// 加锁成功的在上面就已经结束了,所以下面的都是加锁失败时走的// 这里它订阅了一个channel,参数threadId无用,别被误导了,它订阅的名称时固定的// 为什么这里要订阅?可以思考一下CompletableFuture<RedissonLockEntry> future = subscribe(threadId);// 检查是否超时pubSub.timeout(future);RedissonLockEntry entry;// 这里的interruptibly应该时程序一次时,是否结束,而不是一直在执行中if (interruptibly) {entry = commandExecutor.getInterrupted(future);} else {entry = commandExecutor.get(future);}try {// 这里就是一个自旋 + 加锁while (true) {// 每次循环都进行加锁操作ttl = tryAcquire(-1, leaseTime, unit, threadId);// 同样,如果这里时null,那么就是加锁成功了if (ttl == null) {break;}// 加锁失败:返回了锁的过期时间if (ttl >= 0) {try {// 信号量 + unsafe.park // 信号量:本地更改线程共享变量状态达到加锁的目的// unsafe.park:利用park方法将加锁失败(信号量更改失败)的线程进行挂起// 为什么要挂起,这个问题应该不用多说entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else {// 这个分支时ttl < 0,也就是过期时间为负数,也就是锁失效了// 对本地加锁,之后在下一次循环redis加锁if (interruptibly) {entry.getLatch().acquire();} else {entry.getLatch().acquireUninterruptibly();}}}} finally {unsubscribe(future, threadId);}
//        get(lockAsync(leaseTime, unit));}

上面的步骤就是Redisson大致逻辑:

  1. lua脚本加锁,记录线程id,防止非本线程解锁
  2. 成功则退出
  3. 添加订阅对应的channel(这里的订阅是异步的)
    1. 唤醒:当收到channel的通知后,也就是上一个锁解锁了
    2. 从第6步醒来
  4. 自旋
  5. lua脚本加锁(与第一步一样),存在订阅后,上一个锁解锁了,就不用再挂起线程
  6. 失败挂起:unsafe.park
  7. 加锁成功后,取消订阅

lua脚本加锁

image-20240402175746999image-20240402175814387

image-20240403103101607

简单说一下这个脚本做了什么(lua脚本类似js):

redis.call是调用redis命令,第一个参数是redis的命令,第二个参数是参数;

它先是exists判断了key,以及hash数据结构了的field是否存在,

如果存在,对field递增,为什么要递增?思考一下;并设置过期时间,返回返回nil

如果不存在,调用redis命令pttl,获取key的过期时间,并返回;

那如果我们自己写lua脚本呢?

redis 2.6之后支持lua脚本,一个脚本,执行这个脚本是原子性的,所以脚本里的多个命令是原子性的。

如果说要执行批量的命令,可以使用piple,但是管道的话,它并不是原子性的,他只是一次性把批量的命令发给了redis。

LUA脚本格式:

eval "脚本 KEYS[1...N] ARGV[1...N]" count key[1...N] argv[1...N]

KEYS[1…N]:key的展位符,多个时,序号递增,从1开始

ARGV[1…N]:value的展位符,多个时,序号递增,从1开始

count:是对应key输入的个数

key[1…N]:对应KEYS[0…N]的key

argv[1…N]:对应ARGV[0…N]的value

这里就大概简单的说明了一下,使得看本篇的朋友能够理解,详细的还请百度;

看门狗

使用过Redisson的朋友应该都听过“看门狗”,为什么Redisson加锁要看门狗呢?

如果我们使用lock()方法,不设置过期时间,那么应该是永不过期;

好,如果说,加锁成功,在解锁时出了意外,如服务异常退出,或宕机,导致没有解锁,那么这个锁就需要人工干预了,这是有问题的。

所以,在Redisson中,它并不是永不过期,当我们使用lock()方法时,它的参数时-1,但在最终执行lua脚本时传入了默认参数:

位置:org.redisson.RedissonLock#tryAcquireAsync

image-20240402205409341

过期时间时:internalLockLeaseTime;

image-20240402205832964image-20240402205846003

image-20240402205902393

可以看到在初始化时,它便赋予了该值一个默认的30秒;

它并没有将锁设置为无限时间,而是30秒,又怎么保证锁的有效?

所以它还有一个续约的操作,对未使用unlock的锁进行时间延迟,这一操作是为了保证未来某一时刻如果出现服务或其他问题导致解锁失败,产生死锁这样的一个情况。

来看代码:

 private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;// leaseTime=-1表示永不过期if (leaseTime > 0) {// 对应方法:tryLock(过期时间, 时间单位)ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 对应方法:lock()// 注意它达到参数是:internalLockLeaseTime,上面说了他是30秒// internalLockLeaseTime = lockWatchdogTimeout = 30 * 1000;ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 这里是上面ttlRemainingFuture回调结果处理,// 如果出现异常,就unlock解锁CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);
// 这里是定时刷新任务CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// 执行成功返回的时null,所以这里以null为加锁成功的标志if (ttlRemaining == null) {if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}

image-20240403105722608

image-20240403105635897

handleNoSync它只是针对异常做了处理,正常情况下只是进行了封装,而unlocakInnerAysnc也是在异常时的一个回调;

return是结果封装;

image-20240403105756315

再回到刷新的部分:

它是在加锁完后的一个回调方法,ttlRemaining它就是上面执行的结果,null或者是过期时间

image-20240403110155068

 private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;// leaseTime=-1表示永不过期if (leaseTime > 0) {// 对应方法:tryLock(过期时间, 时间单位)ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 对应方法:lock()// 注意它达到参数是:internalLockLeaseTime,上面说了他是30秒// internalLockLeaseTime = lockWatchdogTimeout = 30 * 1000;ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 这里是上面ttlRemainingFuture回调结果处理,// 如果出现异常,就unlock解锁CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);
// 这里是定时刷新任务CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// 执行成功返回的时null,所以这里以null为加锁成功的标志if (ttlRemaining == null) {if (leaseTime > 0) {// leaseTime > 0 表示加锁时设置了过期时间// 而ternalLockLeaseTime存在默认值,这里就是取消了默认值internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 那,这里执行 过期时间的定时刷新scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);}

image-20240403144946555

过期时间刷新的步骤:

  1. 创建ExpirationEntry,可以看作时一个上下文对象,他是延时的标识
  2. 原子操作,添加上下文ExpirationEntry
  3. 设置当前线程id到上下文
  4. 创建Timeout延时任务,延时10秒
  5. 延时任务执行(异步):
    1. 根据客户端id和锁获取上下文
    2. 通过上下文获取到线程id
    3. 异步执行延时lua脚本
    4. 执行回调:成功,回调本身,回调第四步,失败,移除上下文,取消延时
 protected void scheduleExpirationRenewal(long threadId) {// 1. 创建ExpirationEntry,可以看作时一个上下文对象,他是延时的标识ExpirationEntry entry = new ExpirationEntry();// 2. 原子操作,添加上下文ExpirationEntry// 要进行过期时间刷新,就要先添加这个对象,可以理解为一个刷新的标志ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);// 3. 设置当前线程id到上下文if (oldEntry != null) {// 不为空,说明这个对象已经存在,已经有其他线程执行了过期刷新oldEntry.addThreadId(threadId);} else {// 为空,则是不存在该映射对象(锁不存在)entry.addThreadId(threadId);try {// 执行过期时间刷新// 4. 创建Timeout延时任务,延时10秒// 5. 延时任务执行(异步)://    5.1 根据客户端id和锁获取上下文//    5.2 通过上下文获取到线程id//    5.3 异步执行延时lua脚本//    5.4 执行回调:成功,回调本身,回调第四步,失败,移除上下文,取消延时renewExpiration();} finally {if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}}

下面我们进入renewExpiration方法:

 private void renewExpiration() {// EXPIRATION_RENEWAL_MAP 在上一层方法中添加了刷新的上下文标志ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {// 如果这里为null,就说明已经有其他线程移除了,那么就是不需要刷新,锁不存在了return;}// 4. 创建Timeout延时任务,延时10秒// 注意这里的时间: internalLockLeaseTime / 3// 之前说过:internalLockLeaseTime = lockWatchdogTimeout = 30 * 1000;// 所以这里是延迟10秒Timeout task = getServiceManager().newTimeout(new TimerTask() {// 5. 延时任务执行(异步)@Overridepublic void run(Timeout timeout) throws Exception {// 5.1 根据客户端id和锁获取上下文// entryName是客户端id+线程id// 每次执行都要检查,刷新的标志存在,就说明锁还在,需要执行ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}// 5.2 通过上下文获取到线程idLong threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 5.3 异步执行延时lua脚本CompletionStage<Boolean> future = renewExpirationAsync(threadId);// 当延时完成后,执行这个方法,也是个回调future.whenComplete((res, e) -> {if (e != null) {log.error("Can't update lock {} expiration", getRawName(), e);// 异常时,移除刷新的标志// 也是避免死锁的一个方式EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}// 5.4 成功:回调本身,回调第四步,失败:移除上下文,取消延时if (res) {// 延时成功,回调当前这个方法,以实现定时任务renewExpiration();} else {// 没有成功,也是移除刷新的标志// 这里没有成功的情况,是锁已经不存在了,对应的定时任务就应该停止;// 两个步骤:// 1. task.cancel():通过上下文获取到任务,然后取消// 2. EXPIRATION_RENEWAL_MAP.remove(getEntryName());cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);// 将定时任务放到entry中,也就是放到了定时任务的上下文中// 在下一次时,通过上下文获取到这个定时任务ee.setTimeout(task);}

其实他延时的逻辑也是一个lua脚本:

image-20240403154502286

漏了一点:

由Redisson实现JDK的Timeout类,加回调完成的一个定时任务;当当前任务执行完后,又执行本身方法,创建一个延迟任务,也就实现了一个定时任务

相关文章:

Redisson分布式锁原理解析

前言 首先Redis执行命令是单线程的&#xff0c;所以可以利用Redis实现分布式锁&#xff0c;而对于Redis单线程的问题&#xff0c;是其线程模型的问题&#xff0c;本篇重点是对目前流行的工具Redisson怎么去实现的分布式锁进行深入理解&#xff1b;开始之前&#xff0c;我们可以…...

Linux RS232

一、确认硬件信息 RS232&#xff1a; 引脚信息&#xff1a; 二、软件配置 1、pinctrl信息&#xff1a; 2、设备树节点&#xff1a; 3、修改串口支持的模式 三、驱动 bsp/drivers/uart/sunxi-uart.c 四、烧录测试 查看串口参数&#xff1a; stty -F /dev/ttyAS3 -a stty -F…...

英伟达Docker 安装与GPu镜像拉取

获取nvidia_docker压缩包nvidia_docker.tgz将压缩包上传至服务器指定目录解压nvidia_docker.tgz压缩包 tar -zxvf 压缩包执行rpm安装命令&#xff1a; #查看指定rpm包安装情况 rpm -qa | grep libstdc #查看指定rpm包下的依赖包的版本情况 strings /lib64/libstdc |grep GLI…...

智慧交通的神经中枢:利用ARMxy进行实时交通流数据采集

气候变化和水资源日益紧张&#xff0c;精准农业成为了提高农业生产效率、节约资源的关键。在这一变革中&#xff0c;ARMxy工业计算机扮演了核心角色&#xff0c;特别是在智能灌溉系统的实施中。 背景介绍&#xff1a; 某大型农场面临着灌溉效率低、水资源浪费严重的问题。传统的…...

文心一言使用技巧

前言 文心一言是一款基于人工智能技术的自然语言处理工具&#xff0c;它可以帮助用户生成、编辑和优化各种类型的文本。无论是写作、翻译、总结&#xff0c;还是进行信息提取和数据分析&#xff0c;文心一言都能提供强大的支持。本文将详细介绍文心一言的使用技巧&#xff0c;…...

技术人如何打造研发团队

技术人作为写代码一路走上来&#xff0c;其实不像销售岗位&#xff0c;售后交付岗位与人的打交道那么多。主要是很简单的技术沟通&#xff0c;在慢慢走上管理岗位后&#xff0c;也是依据自己的经验&#xff0c;自己的感觉来管理团队&#xff0c;很多时候自己的事情不但没少&…...

月薪6万,想离职...

大家好&#xff0c;我是无界生长&#xff0c;国内最大AI付费社群“AI破局俱乐部”初创合伙人。这是我的第 39 篇原创文章——《月薪6万&#xff0c;想离职...》 是的&#xff0c;你没有看错&#xff0c;我月薪6万&#xff0c;却想离职&#xff0c;很不可思议吧&#xff1f;周围…...

ReentrantLock底层原理

ReentrantLock public ReentrantLock() {sync new NonfairSync(); }public ReentrantLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync(); }ReentrantLock 的默认实现是非公平锁&#xff0c;实际上 ReentrantLock 中的方法&#xff0c;几乎都让 sync 实现…...

基于JSP的医院远程诊断系统

开头语&#xff1a; 你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果有相关需求&#xff0c;文末可以找到我的联系方式。 开发语言&#xff1a; Java 数据库&#xff1a; MySQL 技术&#xff1a; JSP Servlet JSPBean 工具&#xff1a; IDEA/Eclipse、Navica…...

项目:基于httplib/消息队列负载均衡式在线OJ

文章目录 写在前面关于组件开源仓库和项目上线其他文档说明项目亮点 使用技术和环境项目宏观结构模块实现compiler模块runner模块compile_run模块compile_server模块 基于MVC结构的OJ服务什么是MVC&#xff1f;用户请求服务路由功能Model模块view模块Control模块 写在前面 关于…...

详解python中的pandas.read_csv()函数

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。 &#x1f913; 同时欢迎大家关注其他专栏&#xff0c;我将分享Web前后端开发、人工智能、机器学习、深…...

速盾:DDoS高防IP上设置转发规则

DDoS攻击是一种网络攻击方式&#xff0c;攻击者通过大量请求使目标服务器或网络资源超负荷运行&#xff0c;导致服务不可用。为了保护网络安全&#xff0c;减少DDoS攻击对网络的影响&#xff0c;使用DDoS高防IP可以是一种解决方案。而在DDoS高防IP上设置转发规则可以提高网络的…...

京东一面测开(KPI)

京东一面测开凉经&#xff08;笔试ak&#xff09; 3.8 面试官&#xff1a;你很优秀啊&#xff0c;你不用谦虚 没问技术相关&#xff0c;问了如何设计测试用例步骤一些理论&#xff1a; 什么是软件测试&#xff1f;其目的是什么&#xff1f; 软件测试有哪些类型&#xff1f;请列…...

Django框架中级

Django框架中级 – 潘登同学的WEB框架 文章目录 Django框架中级 -- 潘登同学的WEB框架 中间件自定义中间件常用中间件process_view() 使用中间件进行URL过滤 Django生命周期生命周期分析 Django日志日志配置filter过滤器自定义filter 日志格式化formatter Django信号内置信号定…...

cordova-plugin-inappbrowser内置浏览器插件

一、InAppBrowser(内置浏览器) 允许在在单独的窗口中加载网页。例如要向应用用户展示其他网页。当然可以很容易地在应用中加载网页内容并管理,但有时候需要不同的用户体验,InAppBrowser加载网页内容,应用用户可以更方便的直接返回到主应用。 二、安装命令: cordova pl…...

打造智慧工厂核心:ARMxy工业PC与Linux系统

智能制造正以前所未有的速度重塑全球工业格局&#xff0c;而位于这场革命核心的&#xff0c;正是那些能够精准响应复杂生产需求、高效驱动自动化流程的先进设备。钡铼技术ARMxy工业计算机&#xff0c;以其独特的设计哲学与卓越的技术性能&#xff0c;正成为众多现代化生产线背后…...

Java File IO

Java File IO ~主要介绍四个类 InputStream OutputStream FileReader FileWriter~ InputStream &#xff08;字节流读取File&#xff09; public static void main(String[] args) throws IOException {String filePath "D:\\Javaideaporject\\JavaBaseSolid8\\File\\t…...

MySQL 函数与约束

MySQL 函数与约束 文章目录 MySQL 函数与约束1 函数1.1 字符串函数1.2 数值函数1.3 日期函数1.4 流程函数 2 约束2.1 概述2.2 约束演示2.3 外键约束2.4 删除/更新行为 1 函数 函数是指一段可以直接被另一程序调用的程序或代码。 1.1 字符串函数 MySQL中内置了很多字符串函数&…...

12_1 Linux Yum进阶与DNS服务

12_1 Linux Yum进阶与DNS服务 文章目录 12_1 Linux Yum进阶与DNS服务[toc]1. Yum进阶1.1 自定义yum仓库1.2 网络Yum仓库 2. DNS服务2.1 为什么要使用DNS系统2.2 DNS服务器的功能2.3 DNS服务器分类2.4 DNS服务使用的软件及配置2.5 搭建DNS服务示例2.6 DNS特殊解析 1. Yum进阶 1…...

Spring Boot集成geodesy实现距离计算

1.什么是geodesy&#xff1f; 浩瀚的宇宙中&#xff0c;地球是我们赖以生存的家园。自古以来&#xff0c;人类一直对星球上的位置和彼此的距离着迷。无论是航海探险、贸易往来还是科学研究&#xff0c;精确计算两个地点之间的距离都是至关重要的。 Geodesy&#xff1a;大地测量…...

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…...

Android Wi-Fi 连接失败日志分析

1. Android wifi 关键日志总结 (1) Wi-Fi 断开 (CTRL-EVENT-DISCONNECTED reason3) 日志相关部分&#xff1a; 06-05 10:48:40.987 943 943 I wpa_supplicant: wlan0: CTRL-EVENT-DISCONNECTED bssid44:9b:c1:57:a8:90 reason3 locally_generated1解析&#xff1a; CTR…...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试

作者&#xff1a;Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位&#xff1a;中南大学地球科学与信息物理学院论文标题&#xff1a;BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接&#xff1a;https://arxiv.…...

【网络安全产品大调研系列】2. 体验漏洞扫描

前言 2023 年漏洞扫描服务市场规模预计为 3.06&#xff08;十亿美元&#xff09;。漏洞扫描服务市场行业预计将从 2024 年的 3.48&#xff08;十亿美元&#xff09;增长到 2032 年的 9.54&#xff08;十亿美元&#xff09;。预测期内漏洞扫描服务市场 CAGR&#xff08;增长率&…...

基于数字孪生的水厂可视化平台建设:架构与实践

分享大纲&#xff1a; 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年&#xff0c;数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段&#xff0c;基于数字孪生的水厂可视化平台的…...

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…...

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…...

FFmpeg:Windows系统小白安装及其使用

一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】&#xff0c;注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录&#xff08;即exe所在文件夹&#xff09;加入系统变量…...

为什么要创建 Vue 实例

核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...