Redisson 源码解析 - 分布式锁实现过程

一、Redisson 分布式锁源码解析

Redisson是架设在Redis基础上的一个Java驻内存数据网格。在基于NIONetty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

其中比较具体特色的就是 Redisson 对分布式锁的支持,不仅简化了分布式锁的应用过程还支持 Fair Lock、MultiLock、RedLock、ReadWriteLock 等锁的实现。本文就 Redisson 分布式锁的加锁和解锁过程的源码进行大致的解析。

下面是Redisson 源码地址:

https://github.com/redisson/redisson

如果对 Redisson 的使用还不了解的小伙伴可以先看下下面这篇文章:

https://xiaobichao.blog.csdn.net/article/details/112726748

Redisson 中的分布式锁在使用起来非常简便,例如:

public class TestLock {@ResourceRedissonClient redissonClient;@Testpublic void test() {RLock lock = null;try {// 获取可重入锁lock = redissonClient.getLock("redislock");// 获取锁,如果获取不到会等待lock.lock();Thread.sleep(30000000);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {if (lock != null) {// 释放锁lock.unlock();}}}@Testpublic void test1() {RLock lock = null;try {// 获取可重入锁lock = redissonClient.getLock("redislock");// 尝试获取锁,返回获取锁的状态Boolean isLock = lock.tryLock();Thread.sleep(30000000);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {if (lock != null) {// 释放锁lock.unlock();}}}
}

下面分别从 locktryLockunlock 、三个地方进行源码的解析。

二、lock 获取锁和看门狗机制

先看下 redissonClient.getLock 方法,它默认创建了一个 RedissonLock 对象,并将锁的key传递进来:

在这里插入图片描述

RedissonLock 对象又继承至RedissonBaseLock 类:

在这里插入图片描述

因此我们下面大多的源码分析都基于这两个类进行。

首先进到 RedissonLock 类下的 lock() 方法中:

在这里插入图片描述

这里主要又调用了 lock(long leaseTime, TimeUnit unit, boolean interruptibly) 方法,注意如果没有指定过期时间默认为 -1 ,下面看到 lock(long leaseTime, TimeUnit unit, boolean interruptibly) 方法中:

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {// 当前线程IDlong threadId = Thread.currentThread().getId();// 尝试获取锁,如果已经有锁的话返回锁的剩余时间Long ttl = tryAcquire(-1, leaseTime, unit, threadId);// 获取锁成功if (ttl == null) {return;}// 如果获取锁失败,订阅当前线程,以便后续获取锁时得到通知。CompletableFuture<RedissonLockEntry> future = subscribe(threadId);//设置超时处理,当订阅的future完成时,触发超时处理。pubSub.timeout(future);//定义一个RedissonLockEntry对象,用于表示当前线程在分布式锁中的状态。RedissonLockEntry entry;if (interruptibly) {// 可中断entry = commandExecutor.getInterrupted(future);} else {entry = commandExecutor.get(future);}try {// 循环尝试获取锁while (true) {// 尝试获取锁ttl = tryAcquire(-1, leaseTime, unit, threadId);// 获取锁成功if (ttl == null) {break;}// 如果已经存在锁的过期时间大于等于0,需要等待通知if (ttl >= 0) {try {// 通过Semaphore 的 tryAcquire方法等待指定时间entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else { //如果剩余时间小于0,就一直等待。if (interruptibly) {entry.getLatch().acquire();} else {entry.getLatch().acquireUninterruptibly();}}}} finally {// 无论加锁成功或失败,都取消订阅unsubscribe(entry, threadId);}
}

代码中加了注释,这里我总结下,首先调用 tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) 方法尝试获取锁,如果锁存在的话则返回过期时间,为 null 的话表示获取锁成功。如果获取锁失败,则将自己加入到订阅中,然后开启一个死循环,在循环中再次尝试获取锁,如果还是没有获取到的话则使用 SemaphoretryAcquire 方法阻塞当前线程,如果其他线程释放了锁,则这里继续循环再次尝试获取锁。

下面主要看下 tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) 尝试获取锁的逻辑,看到该方法下:

在这里插入图片描述

tryAcquire 方法又调用了 tryAcquireAsync0 方法,然后又主要调用了 tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) 方法,下面主要看到这个方法下:

private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime > 0) {//如果指定了锁持有时间,则根据指定的时间设置 key 的过期时间ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 没指定,默认锁持有 30sttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 执行 lua 操作CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// 如果加锁成功if (ttlRemaining == null) {if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 没指定的话// 启动看门狗,延长锁持有时间scheduleExpirationRenewal(threadId);}}// 返回锁的过期时间return ttlRemaining;});return new CompletableFutureWrapper<>(f);
}

这里其中 tryLockInnerAsync 方法主要是指定了 Lua 脚本,主要注意的是如果没有指定了锁的过期则默认为 30s 的时间,然后在 Lua 脚本执行后,同样的判断,如果获取到锁的话并且没有指定锁的过期时间则开启看门狗机制,为锁延长时间续命的操作。

这里先看下核心操作 tryLockInnerAsync 方法中 Lua 脚本:

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, command,// 如果锁不存在,或者哈希表中锁对应的线程ID存在的话"if ((redis.call('exists', KEYS[1]) == 0) " +"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +// 对hash中的内容值 +1"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +// 设置过期时间"redis.call('pexpire', KEYS[1], ARGV[1]); " +//表示脚本执行成功,且不需要返回特定的值。"return nil; " +"end; " +// 如果if条件不满足,返回剩余过期时间(以毫秒为单位)"return redis.call('pttl', KEYS[1]);",// 对应这 lua 脚本中的参数,第一个参数就是 KEYS[1],以此类推Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

这里主要利于 Lua 的原子性将整个判断操作过程给原子化了,其中这里锁的结构是以 hash 的形式存放的,key为锁的名称,hash中的key为线程IDUUID+线程ID的形式),因为分布式情况下线程ID也有可能重复,value为数字表示锁重入的次数, lua 脚本如果执行加锁逻辑成功则返回 null,否则返回锁的过期时间,也就对应前面获取锁的时候判断的依据。

下面回到上面的 tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) 方法中,在 ttlRemainingFuture.thenApply 中如果获取锁成功,并且没有指定锁的过期时间则会开启看门狗机制为锁进行续命操作,主要调用的是 scheduleExpirationRenewal(long threadId) 方法,下面看到该方法下的逻辑:

protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();// 加入看门狗记录中ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);// 如果已经存在if (oldEntry != null) {// 重新指定线程IDoldEntry.addThreadId(threadId);} else { // 如果不存在的话就开启看门狗entry.addThreadId(threadId);try {// 启动看门狗renewExpiration();} finally {// 如果线程已经终止,则关闭看门狗if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}
}

主要的逻辑在 renewExpiration() 方法下,继续看到该方法中:

private void renewExpiration() {// 获取当前信息ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}// 执行计时任务Timeout task = getServiceManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {//再次获取信息ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}// 获取线程IDLong threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 延长锁的过期时间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;}if (res) { // 如果执行成功// 递归继续执行renewExpiration();} else { // 执行失败// 关闭看门狗cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

这里主要通过递归延时任务的方式实现循环执行的效果,其中延时的时间为 internalLockLeaseTime 的三分之一,也就是默认 10s 触发一次,在任务中主要通过 renewExpirationAsync(long threadId) 方法,对锁进行了延时续命操作,看到该方法中:

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 如果锁和线程ID存在"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +// 重置过期时间"redis.call('pexpire', KEYS[1], ARGV[1]); " +// 成功返回 1"return 1; " +"end; " +// 失败返回 0"return 0;",// lua 脚本中对应的参数Collections.singletonList(getRawName()),internalLockLeaseTime, getLockName(threadId));
}

这里还是依靠 Lua 脚本的方式,如果锁存在的话就重置过期时间,达到续命的效果。

三、tryLock 获取锁

tryLocklock是两种获取分布式锁的方法,它们的主要区别在于获取锁的方式和阻塞行为。tryLock默认是一种非阻塞的获取锁的方法,也可以通过设置 waitTime 变成阻塞的。而lock默认就是一种阻塞的获取锁的方法。

他们俩的最终处理逻辑都是一样的,只不过默认的 tryLock 没有订阅阻塞的操作。

下面看下默认的 tryLock 的操作 ,进到 RedissonLock 下的 tryLock() 中:

在这里插入图片描述

再进入 tryLockAsync() 方法中:

在这里插入图片描述

这里调用了 tryLockAsync 方法,并将当前线程的ID传递了进来,继续看到 tryLockAsync 方法中:

在这里插入图片描述

在看到 tryAcquireOnceAsync 方法中,注意这里的等待时间和上面 lock() 默认一样,是 -1

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {CompletionStage<Boolean> acquiredFuture;if (leaseTime > 0) {//如果指定了锁持有时间,则根据指定的时间设置 key 的过期时间acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);} else {// 没指定,默认锁持有 30sacquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);}// 执行 lua 操作acquiredFuture = handleNoSync(threadId, acquiredFuture);CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {// 如果加锁成功if (acquired) {// 如果指定了锁持有时间if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else { // 没指定的话,// 看门狗,延长锁持有时间scheduleExpirationRenewal(threadId);}}// 返回获取锁的状态return acquired;});return new CompletableFutureWrapper<>(f);
}

这里的逻辑相比于前面 lock() 的逻辑就差不多了,只不过缺少了订阅和阻塞等待重试的操作,再下面的操作和lock() 的逻辑是一致的。

四、unlock 解锁和关闭看门狗

解锁的逻辑看到 RedissonBaseLock 下的 unlock() 方法中:

在这里插入图片描述

继续看到 unlockAsync 方法中:

在这里插入图片描述

主要逻辑在 unlockAsync0 方法中:

private RFuture<Void> unlockAsync0(long threadId) {// 解锁CompletionStage<Boolean> future = unlockInnerAsync(threadId);CompletionStage<Void> f = future.handle((opStatus, e) -> {// 关闭看门狗cancelExpirationRenewal(threadId);if (e != null) { // 如果执行有异常if (e instanceof CompletionException) {throw (CompletionException) e;}throw new CompletionException(e);}if (opStatus == null) { // 如果结果为空的话,表示锁不存在IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "+ id + " thread-id: " + threadId);throw new CompletionException(cause);}return null;});return new CompletableFutureWrapper<>(f);
}

主要做了两件事,解锁和关闭看门狗,先看下 unlockInnerAsync(long threadId) 方法解锁的过程:

protected final RFuture<Boolean> unlockInnerAsync(long threadId) {String id = getServiceManager().generateId();MasterSlaveServersConfig config = getServiceManager().getConfig();int timeout = (config.getTimeout() + config.getRetryInterval()) * config.getRetryAttempts();timeout = Math.max(timeout, 1);// 解锁RFuture<Boolean> r = unlockInnerAsync(threadId, id, timeout);CompletionStage<Boolean> ff = r.thenApply(v -> {CommandAsyncExecutor ce = commandExecutor;if (ce instanceof CommandBatchService) {ce = new CommandBatchService(commandExecutor);}ce.writeAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.DEL, getUnlockLatchName(id));if (ce instanceof CommandBatchService) {((CommandBatchService) ce).executeAsync();}// 释放锁的结果return v;});return new CompletableFutureWrapper<>(ff);
}

这里的重点主要关注 unlockInnerAsync 方法,通过使用 Lua 脚本进行解锁的操作:

protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 从Redis中获取锁的状态。"local val = redis.call('get', KEYS[3]); " +//如果不是false"if val ~= false then " +//将其转换为数字并返回,也就是 true 返回 1"return tonumber(val);" +"end; " +// 如果哈希表锁中不存在线程ID,表示锁已经被释放,返回nil。"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +//对锁中的线程ID的值减1,并将结果存储在 counter 变量中。这是一个计数器的操作。"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +//如果计数器值大于0,表示锁仍然被持有。"if (counter > 0) then " +// 更新哈希表锁的过期时间。"redis.call('pexpire', KEYS[1], ARGV[2]); " +// 设置键锁的状态值为0,并设置过期时间,表示锁仍然被持有。"redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +//返回0,表示锁仍然被持有"return 0; " +"else " + //如果计数器值不大于0,表示锁即将被释放。//删除锁"redis.call('del', KEYS[1]); " +"redis.call(ARGV[4], KEYS[2], ARGV[1]); " +// 设置键锁的状态值为1,并设置过期时间,表示锁已经被释放。"redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +//返回1,表示锁已经被释放"return 1; " +"end; ",Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
}

需要注意的是,在 Lua 脚本中,如果锁还存在的话,就对 hash 中的 value 减一,如果此时 value 结果还大于 0 的话,则表示这是重入锁的场景,此时不能直接删除锁,而是对重入的次数进行减一,并且要重置过期时间。

下面再回到 unlockAsync0(long threadId) 方法中,释放锁通过 Lua 脚本实现了,下面看下 cancelExpirationRenewal(Long threadId) 关闭看门狗的操作:

protected void cancelExpirationRenewal(Long threadId) {// 从记录中获取信息ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (task == null) {return;}if (threadId != null) {// 移除线程IDtask.removeThreadId(threadId);}if (threadId == null || task.hasNoThreads()) {// 关闭计时任务Timeout timeout = task.getTimeout();if (timeout != null) {timeout.cancel();}// 从缓存记录中删除EXPIRATION_RENEWAL_MAP.remove(getEntryName());}
}

这里就比较好理解了,停止计时任务,从缓存记录中移除。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/606728.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一文搞定JMM核心原理

公众号《鲁大猿》&#xff0c;寻精品资料&#xff0c;帮你构建Java全栈知识体系 www.jiagoujishu.cn &#xff08;架构技术.cn&#xff09; JMM引入 从堆栈说起 JVM内部使用的Java内存模型在线程栈和堆之间划分内存。 此图从逻辑角度说明了Java内存模型&#xff1a; # 堆栈里…

SpringBoot + Mybatis 实现多数据源原来如此简单

1、为什么需要整合多数据源 在开发的过程中&#xff0c;我们可能会遇到一个工程使用多个数据源的情况&#xff0c;总体而言分为以下几个原因 a、数据隔离&#xff1a;将不同的数据存储在不同的数据库中&#xff0c;如多租户场景 b、性能优化&#xff1a;将数据分散到多个数据库…

【野火i.MX6ULL开发板】利用microUSB线烧入Debian镜像

0、前言 烧入Debian镜像有两种方式&#xff1a;SD卡、USB SD卡&#xff1a;需要SD卡&#xff08;不是所有型号都可以&#xff0c;建议去了解了解&#xff09;、SD卡读卡器 USB&#xff1a;需要microUSB线 由于SD卡的网上资料很多了&#xff0c;又因为所需硬件&#xff08;SD卡…

Spring应用的部署与管理

一、前言 部署是将开发好的应用发布到服务器上&#xff0c;使其能够被用户访问的关键步骤。Spring框架提供了灵活的部署选项&#xff0c;本文将介绍Spring应用的常见部署方式和一些建议&#xff0c;帮助开发者顺利将应用投放到生产环境。 二、传统部署方式&#xff1a;WAR包 传…

09Bean的生命周期及循环依赖

Spring其实就是一个管理Bean对象的工厂。它负责对象的创建&#xff0c;对象的销毁等。 所谓的生命周期就是&#xff1a;对象从创建开始到最终销毁的整个过程。 Bean的生命周期之5步 ● 第一步&#xff1a;实例化Bean(无参构造方法执行) ● 第二步&#xff1a;Bean属性赋值(注…

基于YOLOv7算法的高精度实时19类动物目标检测识别系统(PyTorch+Pyside6+YOLOv7)

摘要&#xff1a;基于YOLOv7算法的高精度实时19类动物目标检测系统可用于日常生活中检测与定位19类动物目标&#xff08;水牛、 斑马、 大象、 水豚、 海龟、 猫、 奶牛、 鹿、 狗、 火烈鸟、 长颈鹿、 捷豹、 袋鼠、 狮子、 鹦鹉、 企鹅、 犀牛、 羊和老虎&#xff09;&#x…

[Kubernetes]5. k8s集群StatefulSet详解,以及数据持久化(SC PV PVC)

前面通过deployment结合service来部署无状态的应用,下面来讲解通过satefulSet结合service来部署有状态的应用 一.StatefulSet详解 1.有状态和无状态区别 无状态: 无状态(stateless)、牲畜(cattle)、无名(nameless)、可丢弃(disposable) 有状态: 有状态(stateful)、宠物(pet)…

【Python机器学习】构造决策树

通常来说&#xff0c;构造决策树直到所有叶结点都是纯的叶结点&#xff0c;但这会导致模型非常复杂&#xff0c;并且对于训练数据高度过拟合。 为了防止过拟合&#xff0c;有两种常见策略&#xff1a; 1、尽早停止树的生长&#xff0c;也叫预剪枝 2、先构造树&#xff0c;但…

系统架构设计师教程(十)软件可靠性基础知识

软件可靠性基础知识 10.1 软件架构演化和定义的关系10.1.1 演化的重要性10.1.2 演化和定义的关系 10.2 面向对象软件架构演化过程10.2.1 对象演化10.2.2 消息演化10.2.3 复合片段演化10.2.4 约束演化 10.3 软件架构演化方式的分类10.3.1 软件架构演化时期10.3.2 软件架构静态演…

免费服务器腾讯云_腾讯云免费服务器申请流程(2024更新)

腾讯云免费服务器申请入口 https://curl.qcloud.com/FJhqoVDP 免费服务器可选轻量应用服务器和云服务器CVM&#xff0c;轻量配置可选2核2G3M、2核8G7M和4核8G12M&#xff0c;CVM云服务器可选2核2G3M和2核4G3M配置&#xff0c;腾讯云百科txybk.com分享2024年最新腾讯云免费服务器…

径向基函数插值

一、径向基函数的定义 如果 ∣ ∣ x 1 ∣ ∣ ∣ ∣ x 2 ∣ ∣ ||x_1||||x_2|| ∣∣x1​∣∣∣∣x2​∣∣&#xff0c;那么 ϕ ( x 1 ) ϕ ( x 2 ) \phi(x_1)\phi(x_2) ϕ(x1​)ϕ(x2​) 的函数 ϕ \phi ϕ 就是径向函数&#xff0c;即仅由 r ∣ ∣ x ∣ ∣ r||x|| r∣∣…

金蝶EAS pdfviewlocal 任意文件读取漏洞复现

0x01 产品简介 金蝶EAS 为集团型企业提供功能全面、性能稳定、扩展性强的数字化平台&#xff0c;帮助企业链接外部产业链上下游&#xff0c;实现信息共享、风险共担&#xff0c;优化生态圈资源配置&#xff0c;构筑产业生态的护城河&#xff0c;同时打通企业内部价值链的数据链…

《中学物理奇妙日志——30天物理学探索之旅》提纲

《中学物理奇妙日志——30天物理学探索之旅》提纲 第一部分&#xff1a;物理学基础&#xff08;第1-5天&#xff09; 第一天&#xff1a;引言 - 从生活中的物理现象出发&#xff0c;阐述物理学的定义与重要性 子主题&#xff1a;物理学的历史、发展及在现代生活中的广泛应用 …

视图与索引连表查询内/外联和子查询

1.视图 先介绍一下视图&#xff1a; 从SQL的角度来看&#xff0c;视图和表是相同的&#xff0c;两者的区别在于表中存储的是实际的数据&#xff0c;而视图中保存的是SELECT语句&#xff08;视图本身并不存储数据&#xff09;。 使用视图可以轻松完成跨多表查询数据等复杂操作…

大学生如何当一个程序员——第三篇:热门专业学习之路5

第三篇&#xff1a;热门专业学习之路5 1.WEB前端快速入门2.JavaScript基础与深入解析3.jQuery应用与项目开发4.PHP、数据库编程与设计5. Http服务于Ajax编程6. 做一个阶段项目7. H5新特性与移动端开发8.高级框架9.微信小程序 各位小伙伴想要博客相关资料的话关注公众号&#xf…

R语言(12):绘图

12.1 创建图形 12.1.1 plot函数 plot(c(1,2,3),c(1,2,4)) plot(c(1,2,3),c(1,2,4),"b") plot(c(-3,3),c(-1,5),"n",xlab "x",ylab "y")12.1.2 添加线条&#xff1a;abline()函数 x <- c(1,2,3) y <- c(1,3,8) plot(x,y) lm…

算法28:力扣64题,最小路径和------------样本模型

题目&#xff1a; 给定一个二维数组matrix&#xff0c;一个人必须从左上角出发&#xff0c;最后到达右下角 。沿途只可以向下或者向右走&#xff0c;沿途的数字都累加就是距离累加和 * 返回累加和最小值 思路&#xff1a; 1. 既然是给定二维数组matrix&#xff0c;那么二维数…

寒假前端第一次作业

1、用户注册&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>用户注册</title> …

C++中的返回值优化(RVO)

一、命名返回值优化&#xff08;NRVO&#xff09; 是Visual C2005及之后版本支持的优化。 具体来说&#xff0c;就是一个函数的返回值如果是一个对象。那么&#xff0c;正常的返回语句的执行过程是&#xff0c;把这个对象从当前函数的局部作用域&#xff0c;或者叫当前函数的…

视频AI智剪方法:快速批量处理视频,批量剪辑视频的操作

随着科技的飞速发展&#xff0c;视频内容已是获取信息和娱乐的主要方式之一。对于视频创作者和内容生产者来说&#xff0c;如何快速、高效地处理和剪辑大量视频已成为一项重要的需求。现在借助AI技术的不断发展&#xff0c;可以更加智能、高效的处理视频。下面来看云炫AI智剪如…