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

分布式锁常见实现方案

分布式锁常见实现方案

基于 Redis 实现分布式锁

如何基于 Redis 实现一个最简易的分布式锁?

不论是本地锁还是分布式锁,核心都在于“互斥”。

在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNXSET if Not eXists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0

释放锁的话,直接通过 DEL 命令删除对应的 key 即可。

> DEL lockKey
(integer) 1

为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。

选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

Redis 实现简易分布式锁

这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。

为什么要给锁设置一个过期时间?

为了避免锁无法被释放,我们可以想到的一个解决办法就是:给这个 key(也就是锁) 设置一个过期时间

127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
  • lockKey:加锁的锁名;
  • uniqueValue:能够唯一标示锁的随机字符串;
  • NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
  • EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。

这样确实可以解决问题,不过,这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。

如何实现锁的优雅续期?

对于 Java 开发的小伙伴来说,已经有了现成的解决方案:Redissonopen in new window 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:https://redis.io/topics/distlockopen in new window 。

Distributed locks with Redis

Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。

Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

Redisson 看门狗自动续期

看门狗名字的由来于 getLockWatchdogTimeout() 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒(redisson-3.17.6open in new window)。

//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {this.lockWatchdogTimeout = lockWatchdogTimeout;return this;
}
public long getLockWatchdogTimeout() {return lockWatchdogTimeout;
}

renewExpiration() 方法包含了看门狗的主要逻辑:

private void renewExpiration() {//......Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {//......// 异步续期,基于 Lua 脚本CompletionStage<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) {// 无法续期log.error("Can't update lock " + getRawName() + " expiration", e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// 递归调用实现续期renewExpiration();} else {// 取消续期cancelExpirationRenewal(null);}});}// 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);}

默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。

Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期:

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return 1; " +"end; " +"return 0;",Collections.singletonList(getRawName()),internalLockLeaseTime, getLockName(threadId));
}

可以看出, renewExpirationAsync 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。

我这里以 Redisson 的分布式可重入锁 RLock 为例来说明如何使用 Redisson 实现分布式锁:

// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();

只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。

// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);

如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。

如何实现可重入锁?

所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 synchronizedReentrantLock 都属于可重入锁。

不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。

可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。

实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 Redisson ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。

img

Redis 如何解决集群情况下分布式锁的可靠性?

为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。

Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

img

针对这个问题,Redis 之父 antirez 设计了 Redlock 算法open in new window 来解决。

img

Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。

Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。

实际项目中不建议使用 Redlock 算法,成本和收益不成正比。

如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 ZooKeeper 来做,只是性能会差一些。

基于 ZooKeeper 实现分布式锁

Redis 实现分布式锁性能较高,ZooKeeper 实现分布式锁可靠性更高。实际项目中,我们应该根据业务的具体需求来选择。

如何基于 ZooKeeper 实现分布式锁?

ZooKeeper 分布式锁是基于 临时顺序节点Watcher(事件监听器) 实现的。

获取锁:

  1. 首先我们要有一个持久节点/locks,客户端获取锁就是在locks下创建临时顺序节点。
  2. 假设客户端 1 创建了/locks/lock1节点,创建成功之后,会判断 lock1是否是 /locks 下最小的子节点。
  3. 如果 lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
  4. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。

释放锁:

  1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
  2. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
  3. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。

img

实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 ZooKeeper 自带的客户端 zookeeper 来说,Curator 的封装更加完善,各种 API 都可以比较方便地使用。

Curator主要实现了下面四种锁:

  • InterProcessMutex:分布式可重入排它锁
  • InterProcessSemaphoreMutex:分布式不可重入排它锁
  • InterProcessReadWriteLock:分布式读写锁
  • InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
CuratorFramework client = ZKUtils.getClient();
client.start();
// 分布式可重入排它锁
InterProcessLock lock1 = new InterProcessMutex(client, lockPath1);
// 分布式不可重入排它锁
InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2);
// 将多个锁作为一个整体
InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));if (!lock.acquire(10, TimeUnit.SECONDS)) {throw new IllegalStateException("不能获取多锁");
}
System.out.println("已获取多锁");
System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
try {// 资源操作resource.use();
} finally {System.out.println("释放多个锁");lock.release();
}
System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
client.close();

为什么要用临时顺序节点?

每个数据节点在 ZooKeeper 中被称为 znode,它是 ZooKeeper 中数据的最小单元。

我们通常是将 znode 分为 4 大类:

  • 持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
  • 临时(EPHEMERAL)节点:临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点只能做叶子节点 ,不能创建子节点。
  • 持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 /node1/app0000000001/node1/app0000000002
  • 临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。

可以看出,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。

使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。

假设不使用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。

为什么要设置对前一个节点的监听?

Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。

同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。

这个事件监听器的作用是:当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java 中的 wait/notifyAll ),让它尝试去获取锁,然后就成功获取锁了。

如何实现可重入锁?

这里以 Curator 的 InterProcessMutex 对可重入锁的实现来介绍(源码地址:InterProcessMutex.javaopen in new window)。

当我们调用 InterProcessMutex#acquire方法获取锁的时候,会调用InterProcessMutex#internalLock方法。

// 获取可重入互斥锁,直到获取成功为止
@Override
public void acquire() throws Exception {if (!internalLock(-1, null)) {throw new IOException("Lost connection while trying to acquire lock: " + basePath);}
}

internalLock 方法会先获取当前请求锁的线程,然后从 threadData( ConcurrentMap<Thread, LockData> 类型)中获取当前线程对应的 lockDatalockData 包含锁的信息和加锁的次数,是实现可重入锁的关键。

第一次获取锁的时候,lockDatanull。获取锁成功之后,会将当前线程和对应的 lockData 放到 threadData

private boolean internalLock(long time, TimeUnit unit) throws Exception {// 获取当前请求锁的线程Thread currentThread = Thread.currentThread();// 拿对应的 lockDataLockData lockData = threadData.get(currentThread);// 第一次获取锁的话,lockData 为 nullif (lockData != null) {// 当前线程获取过一次锁之后// 因为当前线程的锁存在, lockCount 自增后返回,实现锁重入.lockData.lockCount.incrementAndGet();return true;}// 尝试获取锁String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());if (lockPath != null) {LockData newLockData = new LockData(currentThread, lockPath);// 获取锁成功之后,将当前线程和对应的 lockData 放到 threadData 中threadData.put(currentThread, newLockData);return true;}return false;
}

LockDataInterProcessMutex中的一个静态内部类。

private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();private static class LockData
{// 当前持有锁的线程final Thread owningThread;// 锁对应的子节点final String lockPath;// 加锁的次数final AtomicInteger lockCount = new AtomicInteger(1);private LockData(Thread owningThread, String lockPath){this.owningThread = owningThread;this.lockPath = lockPath;}
}

如果已经获取过一次锁,后面再来获取锁的话,直接就会在 if (lockData != null) 这里被拦下了,然后就会执行lockData.lockCount.incrementAndGet(); 将加锁次数加 1。

整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。

作者声明

如有问题,欢迎指正!

相关文章:

分布式锁常见实现方案

分布式锁常见实现方案 基于 Redis 实现分布式锁 如何基于 Redis 实现一个最简易的分布式锁&#xff1f; 不论是本地锁还是分布式锁&#xff0c;核心都在于“互斥”。 在 Redis 中&#xff0c; SETNX 命令是可以帮助我们实现互斥。SETNX 即 SET if Not eXists (对应 Java 中…...

26、pytest使用allure解读

官方实例 # content of pytest_quick_start_test.py import allurepytestmark [allure.epic("My first epic"), allure.feature("Quick start feature")]allure.id(1) allure.story("Simple story") allure.title("test_allure_simple_te…...

Uncle Maker: (Time)Stamping Out The Competition in Ethereum

目录 笔记后续的研究方向摘要引言贡献攻击的简要概述 Uncle Maker: (Time)Stamping Out The Competition in Ethereum CCS 2023 笔记 本文对以太坊 1 的共识机制进行了攻击&#xff0c;该机制允许矿工获得比诚实同行更高的挖矿奖励。这种名为“Uncle Maker”的攻击操纵区块时间…...

浅谈可重入与线程安全

文章目录 可重入与线程安全的关系 可重入 若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另一段代码&#xff0c;这段代码又使用了该副程序不会出错”&#xff0c;则称其为可重入&#xff08;reentrant 或 re-entrant&#xff09;的。即当该副程序正在运作时&…...

深入理解TDD(测试驱动开发):提升代码质量的利器

在日常的软件开发工作中&#xff0c;我们常常会遇到这样的问题&#xff1a;如何在繁忙的项目进度中&#xff0c;保证我们的代码质量&#xff1f;如何在不断的迭代更新中&#xff0c;避免引入新的错误&#xff1f;对此&#xff0c;有一种有效的开发方式能帮助我们解决这些问题&a…...

pyqt5使用pyqtgraph实现动态热力图

pyqt5使用pyqtgraph实现动态热力图 一、效果图 二、流程 1、打开Designer创建一个UI界面 2、把UI转成py 3、创建一个main.py文件 4、在main文件中渲染画布、创建初始数据、画热力图、创建更新数据线程、绑定按钮触发事件三、UI界面 其中h_map.py代码如下: # -*- coding: ut…...

【android开发-16】android中文件和sharedpreferences数据存储详解

1&#xff0c;文件读写方式的数据存储 下面是一个简单的示例&#xff0c;演示如何在Android中使用内部存储来保存和读取文件&#xff1a; 保存文件&#xff1a; try { String data "这是要保存的数据"; FileOutputStream fos openFileOutput("myFile"…...

《当代家庭教育》期刊论文投稿发表简介

《当代家庭教育》杂志是家庭的参谋和助手&#xff0c;社会的桥梁和纽带&#xff0c;人生的伴侣和知音&#xff0c;事业的良师益友。 国家新闻出版总署批准的正规省级教育类G4期刊&#xff0c;知网、维普期刊网收录。安排基础教育相关稿件&#xff0c;适用于评职称时的论文发表…...

【操作教程】如何将外省医保转入广州市区(医保转移接续手续办理)?

登录&#xff08;可以用微信扫码采用粤省事账号登录&#xff0c;没有粤省事小程序账号的可以自主申请很方便&#xff09;广东政务服务网https://www.gdzwfw.gov.cn/ 这里不得不吐槽官网开发者&#xff0c;太拉胯了&#xff0c;居然有undefined&#xff0c;多刷新几次就好了&…...

【分布式系统学习】CAP原理详解

CAP原理详解 前言CAP一张图 一、概念1.1 关键词解读1.2 关于CAP&#xff08;拆分解读&#xff09;1.3 CAP原理精髓 二、CAP模拟场景举例理解三、CAP原理证明为什么不能同时满足&#xff08;下面举例说明&#xff09;3.1 必须满足分区容错性P下的处理方式3.2 不是必须满足分区容…...

【聚类】K-modes和K-prototypes——适合离散数据的聚类方法

应用场景&#xff1a; 假设一批数据&#xff0c;每一个样本中&#xff0c;有唯一标识&#xff08;id&#xff09;、品类&#xff08;cate_id&#xff09;、受众&#xff08;users, 小孩、老人、中年等&#xff09;等属性&#xff0c;希望从其中找出一些样本&#xff0c;使得这…...

Python-炸弹人【附完整源码】

炸弹人 炸弹人是童年的一款经典电子游戏&#xff0c;玩家控制一个类似"炸弹人"的角色&#xff0c;这个角色可以放置炸弹&#xff0c;并在指定的时间内引爆它们消灭敌人以达到目标&#xff0c;此游戏共设有两节关卡&#xff0c;代码如下&#xff1a; 运行效果&#x…...

[英语学习][5][Word Power Made Easy]的精读与翻译优化

[序言] 今日完成第18页的阅读, 发现大量的翻译错误以及不准确. 需要分两篇文章进行讲解. [英文学习的目标] 提升自身的英语水平, 对日后编程技能的提升有很大帮助. 希望大家这次能学到东西, 同时加入我的社区讨论与交流英语相关的内容. [原著英文与翻译版对照][第18页] Wh…...

Apache Doris 详细教程(一)

1、Doris简介 1.1、doris概述 Apache Doris 由百度大数据部研发&#xff08;之前叫百度 Palo&#xff0c;2018 年贡献到 Apache 社区后&#xff0c; 更名为 Doris &#xff09;&#xff0c;在百度内部&#xff0c;有超过 200 个产品线在使用&#xff0c;部署机器超过 1000 台…...

【Vue3从入门到项目实现】RuoYi-Vue3若依框架前端学习——登录页面

若依官方的前后端分离版中&#xff0c;前端用的Vue2&#xff0c;这个有人改了Vue3的前端出来。刚好用来学习&#xff1a; https://gitee.com/weifengze/RuoYi-Vue3 运行前后端项目 首先运行项目 启动前端&#xff0c;npm install、npm run dev 启动后端&#xff0c;按教程配置…...

win11 关闭快速启动,解决重启后部分应用没有关闭的问题

鼠标右击win11开始菜单选择windows终端&#xff08;管理员&#xff09;打开输入&#xff1a;powercfg /h off按下回车即可...

python爬虫-某公开数据网站实例小记

注意&#xff01;&#xff01;&#xff01;&#xff01;某XX网站逆向实例仅作为学习案例&#xff0c;禁止其他个人以及团体做谋利用途&#xff01;&#xff01;&#xff01; 第一步&#xff1a;分析页面和请求方式 此网站没有技巧的加密&#xff0c;仅是需要携带cookie和请求…...

还记得当初自己为什么选择计算机?

还记得当初自己为什么选择计算机&#xff1f; 当初你问我为什么选择计算机&#xff0c;我笑着回答&#xff1a;“因为我梦想成为神奇的码农&#xff01;我想像编织魔法一样编写程序&#xff0c;创造出炫酷的虚拟世界&#xff01;”谁知道&#xff0c;我刚入门的那天&#xff0…...

“数”说新语向未来 | GBASE南大通用2023媒体交流会成功举办

在当前国家信创战略加速实施&#xff0c;及国民经济数字化转型&#xff0c;叠加驱动信息化行业加速发展的大形势下&#xff0c;以“数说新语-GBASE南大通用开放创新再领航”为主题的2023 GBASE南大通用媒体交流日活动在GBASE天津总部举行。来自IT168、ITPUB、韩锋频道、自主可控…...

每天一点python——day88

#每天一点Python——88 #编程两大思想【面向过程与面向对象】 #如图&#xff1a; 面向过程的线性思维&#xff1a; 类似于做菜一步步的来&#xff0c;先怎么样怎么样&#xff0c;再怎么样 如果不一步步的来&#xff0c;例如先炒菜再点火&#xff0c;这样是做不好的 面向对象&a…...

Spark 之 入门讲解详细版(1)

1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室&#xff08;Algorithms, Machines, and People Lab&#xff09;开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目&#xff0c;8个月后成为Apache顶级项目&#xff0c;速度之快足见过人之处&…...

React hook之useRef

React useRef 详解 useRef 是 React 提供的一个 Hook&#xff0c;用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途&#xff0c;下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

React Native 开发环境搭建(全平台详解)

React Native 开发环境搭建&#xff08;全平台详解&#xff09; 在开始使用 React Native 开发移动应用之前&#xff0c;正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南&#xff0c;涵盖 macOS 和 Windows 平台的配置步骤&#xff0c;如何在 Android 和 iOS…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂

蛋白质结合剂&#xff08;如抗体、抑制肽&#xff09;在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上&#xff0c;高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术&#xff0c;但这类方法普遍面临资源消耗巨大、研发周期冗长…...

centos 7 部署awstats 网站访问检测

一、基础环境准备&#xff08;两种安装方式都要做&#xff09; bash # 安装必要依赖 yum install -y httpd perl mod_perl perl-Time-HiRes perl-DateTime systemctl enable httpd # 设置 Apache 开机自启 systemctl start httpd # 启动 Apache二、安装 AWStats&#xff0…...

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

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

1.3 VSCode安装与环境配置

进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件&#xff0c;然后打开终端&#xff0c;进入下载文件夹&#xff0c;键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)

骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术&#xff0c;它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton)&#xff1a;由层级结构的骨头组成&#xff0c;类似于人体骨骼蒙皮 (Mesh Skinning)&#xff1a;将模型网格顶点绑定到骨骼上&#xff0c;使骨骼移动…...

浅谈不同二分算法的查找情况

二分算法原理比较简单&#xff0c;但是实际的算法模板却有很多&#xff0c;这一切都源于二分查找问题中的复杂情况和二分算法的边界处理&#xff0c;以下是博主对一些二分算法查找的情况分析。 需要说明的是&#xff0c;以下二分算法都是基于有序序列为升序有序的情况&#xf…...