Redis(十四)【Redisson分布式锁基础介绍】

news/2024/3/28 18:28:12/文章来源:https://blog.csdn.net/Wei_Naijia/article/details/129693379

分布式锁 Redisson

一、Redisson 概述


什么是 Redisson

Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。

Redisson 的宗旨是促进使用者对 Redis 的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

一个基于Redis实现的分布式工具,有基本分布式对象和高级又抽象的分布式服务,为每个试图再造分布式轮子的程序员带来了大部分分布式问题的解决办法。

Redisson 和 Jedis、Lettuce区别

Redisson 和它俩的区别就像一个用鼠标操作图形化界面,一个用命令行操作文件。Redisson 是更高层的抽象,Jedis 和 Lettuce 是 Redis 命令的封装

  • Jedis 是 Redis 官方推出的用于通过 Java 连接 Redis 客户端的一个工具包,提供了 Redis 的各种命令支持
  • Lettuce 是一种可扩展的线程安全的 Redis 客户端,通讯框架基于 Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和 Redis 数据模型。Spring Boot 2.x 开始 Lettuce 已取代 Jedis 成为首选 Redis 的客户端。
  • Redisson 是架构在 Redis 基础上,通讯基于 Netty 的综合的、新型的中间件,企业级开发中使用 Redis 的最佳范本

Jedis 把 Redis 命令封装好,Lettuce 则进一步有了更丰富的 Api,也支持集群等模式。但是两者也都点到为止,只给了操作 Redis 数据库的脚手架,而 Redisson 则是基于 Redis、Lua 和 Netty 建立起了成熟的分布式解决方案,甚至 redis 官方都推荐的一种工具集

二、分布式锁


实现分布式锁

分布式锁是并发业务下的必要,虽然实现五花八门:有根据 ZooKeeper 的 Znode 顺序节点,数据库有表级锁和乐/悲观锁,Redis 有 setNx,但是殊途同归,最终还是要回到互斥上来,本篇介绍 Redisson,那就以 redis 为例。

怎么写一个简单的 Redis 分布式锁?

Spring Data Redis 为例,用 RedisTemplate 来操作 Redis(setIfAbsent 已经是 setNx + expire 的合并命令),如下

@Autowired
private RedisTemplate<String, Object> redisTemplate;// 加锁
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}// 解锁,防止多线程情况下删错别人的锁,以uuid为value校验是否自己的锁
public void unlock(String lockName, String uuid) {if(uuid.equals(redisTemplate.opsForValue().get(lockName)){        redisTemplate.opsForValue().del(lockName);}
}// 结构
if(tryLock) {// todo
}finally {unlock;
}

问题】简单1.0版本完成,一眼看出,这是锁没错,但 get 和 del 操作并非原子性,并发一旦大了,无法保证进程安全。于是决定使用 Lua 脚本

2.1 Lua 脚本是什么?

Lua 脚本是 redis 已经内置的一种轻量小巧语言,其执行是通过 redis 的 eval /evalsha 命令来运行,把操作封装成一个 Lua 脚本,如论如何都是一次执行的原子操作

  • 于是2.0版本通过Lua脚本删除,脚本如下
-- 操作的键
local redisKey = KEYS[1];
-- 操作的值
local redisValue = ARGV[1];
if redis.call('get', redisKey) == redisValue then -- 执行删除操作return redis.call('del', redisKey) else -- 不成功,返回0return 0 
end
  • 使用 Java 执行删除脚本
// 解锁脚本
DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua")));// 执行lua脚本解锁
redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);

问题】2.0似乎更像一把锁,但好像又缺少了什么,在Java当中 synchronized 和 ReentrantLock 有两个共同的点,就是他们都是可重入锁,一个线程多次拿锁也不会死锁,需要设计可重入的功能

2.2 可重入锁

如何保证可重入?

可重入就是,同一个线程多次获取同一把锁是允许的,不会造成死锁,这一点 synchronized 偏向锁提供了很好的思路,synchronized 的实现重入是在 JVM 层面,Java 对象头 Mark Word 域中便藏有线程ID和计数器来对当前线程做重入判断,避免每次CAS

偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单测试一下对象头的Mark Word域里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁标志是否设置成1:没有则CAS竞争;设置了,则CAS将对象头偏向锁指向当前线程

再维护一个计数器,同个线程进入则自增1,离开再减1,直到为0才能释放

Redisson 可重入锁的设计

了解原理之后,需要对Lua脚本进行改造,过程如下

1. 需要存储 锁名称lockName、获得该锁的线程id和对应线程的进入次数count2. 加锁
- 每次线程获取锁时,判断是否已存在该锁
- 不存在- 设置hash的key为线程id,value初始化为1- 设置过期时间- 返回获取锁成功true
- 存在- 继续判断是否存在当前线程id的hash key- 存在,线程key的value + 1,重入次数增加1,设置过期时间- 不存在,返回加锁失败
3. 解锁
- 每次线程来解锁时,判断是否已存在该锁
- 存在- 是否有该线程的id的hash key,有则减1,无则返回解锁失败- 减1后,判断剩余count是否为0,为0则说明不再需要这把锁,执行del命令删除

使用的数据结构—Hash

为了方便维护这个对象,我们用Hash结构来存储这些字段。Redis 的 Hash 类似Java的 HashMap,适合存储对象,redis操作命令如下

# 在redis中的hash数据结构当中添加一个以theadId为key,1为value的值
hset lockname threadId 1# 获取lockname的threadId的值
hget lockname threadId# 存储结构为
lockname 锁名称key1:   threadId   唯一键,线程idvalue1:  count     计数器,记录该线程获取锁的次数

计数器的加减

当同一个线程获取同一把锁时,我们需要对对应线程的计数器count做加减,判断一个redis key是否存在,可以用exists,而判断一个 hash 的 key 是否存在,可以用hexists,而 redis 也有 hash 自增的命令hincrby

# 每次自增1时
hincrby lockname threadId 1
# 自减1时 
hincrby lockname threadId -1

解锁的判断

当一把锁不再被需要了,每次解锁一次,count减1,直到为0时,执行删除

综合上述的存储结构和判断流程,加锁和解锁Lua脚本如下

  • 加锁 lock.lua
local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];-- lockname不存在
if(redis.call('exists', key) == 0) thenredis.call('hset', key, threadId, '1');redis.call('expire', key, releaseTime);return 1;
end;-- 当前线程已id存在
if(redis.call('hexists', key, threadId) == 1) thenredis.call('hincrby', key, threadId, 1);redis.call('expire', key, releaseTime);return 1;
end;
return 0;
  • 解锁 unlock.lua
local key = KEYS[1];
local threadId = ARGV[1];-- lockname、threadId不存在
if (redis.call('hexists', key, threadId) == 0) thenreturn nil;
end;-- 计数器-1
local count = redis.call('hincrby', key, threadId, -1);-- 计数器为0,删除lock
if (count == 0) thenredis.call('del', key);return nil;
end;
  • 使用 Java 操作加锁和解锁脚本
/*** @description 原生redis实现分布式锁**/
@Getter
@Setter
public class RedisLock {private RedisTemplate redisTemplate;private DefaultRedisScript<Long> lockScript;private DefaultRedisScript<Object> unlockScript;public RedisLock(RedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;// 加载加锁的脚本(对应resources目录下)lockScript = new DefaultRedisScript<>();this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));this.lockScript.setResultType(Long.class);// 加载释放锁的脚本(对应resources目录下)unlockScript = new DefaultRedisScript<>();this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));}/*** 获取锁*/public String tryLock(String lockName, long releaseTime) {// 存入的线程信息的前缀String key = UUID.randomUUID().toString();// 执行脚本Long result = (Long) redisTemplate.execute(lockScript,Collections.singletonList(lockName),key + Thread.currentThread().getId(),releaseTime);if (result != null && result.intValue() == 1) {return key;} else {return null;}}/*** 解锁* @param lockName* @param key*/public void unlock(String lockName, String key) {redisTemplate.execute(unlockScript,Collections.singletonList(lockName),key + Thread.currentThread().getId());}
}// 执行的redisTemplate调用的函数如下
@Override
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {return scriptExecutor.execute(script, keys, args);
}

至此已经完成了一把分布式锁,拥有互斥、可重入、防死锁的基本特点

问题】A进程在获取到锁的时候,因业务操作时间太长,锁释放了但是业务还在执行,而此刻B进程又可以正常拿到锁做业务操作,两个进程操作就会存在依旧有共享资源的问题,而且如果负责储存这个分布式锁的Redis节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态

所以希望在这种情况时,可以延长锁的releaseTime延迟释放锁来直到完成业务期望结果,这种不断延长锁过期时间来保证业务执行完成的操作就是锁续约

读写分离也是常见,一个读多写少的业务为了性能,常常是有读锁和写锁的

而此刻的扩展已经超出了一把简单轮子的复杂程度,光是处理续约,就给业务带来了难题,何况在性能(锁的最大等待时间)、优雅(无效锁申请)、重试(失败重试机制)等方面还要下功夫研究

三、Redisson 分布式锁


  • 依赖引入
<!-- 原生,本章使用-->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.15.0</version>
</dependency><!-- 另一种Spring集成starter,本章未使用 -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.15.0</version>
</dependency>
  • 配置客户端连接
@Configuration
public class RedissionConfig {@Value("${spring.redis.host}")private String redisHost;@Value("${spring.redis.password}")private String password;@Value("${spring.redis.port}")private int port;@Beanpublic RedissonClient getRedisson() {Config config = new Config();config.useSingleServer().setAddress("redis://" + redisHost + ":" + port).setPassword(password);config.setCodec(new JsonJacksonCodec());return Redisson.create(config);}
}
  • 启用分布式锁
@Resource
private RedissonClient redissonClient;RLock rLock = redissonClient.getLock(lockName);
try {boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS);if (isLocked) {// TODO}
} catch (Exception e) {rLock.unlock();
}

简洁明了,只需要一个 RLock,既然推荐 Redisson,就往里面看看他是怎么实现的

3.1 RLock 锁

RLock 是 Redisson 分布式锁的最核心接口,继承了 concurrent 包的 Lock 接口和自己的 RLockAsync 接口,RLockAsync 的返回值都是 RFuture,是 Redisson 执行异步实现的核心逻辑,也是 Netty 发挥的主要阵地

RLock 如何加锁?

从 RLock 进入,找到 RedissonLock 类,找到 tryLock 方法再推进到主要的 tryAcquireOnceAsync 方法,这是加锁的主要代码(版本不一此处实现有差别,和最新3.15.x有一定出入,但是核心逻辑依然未变。此处以3.15.0为例)

    private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {if (leaseTime != -1L) {return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e == null) {if (ttlRemaining == null) {this.scheduleExpirationRenewal(threadId);}}});return ttlRemainingFuture;}}

此处出现leaseTime时间判断的2个分支,实际上就是加锁时是否设置过期时间,未设置过期时间(-1)时则会有 watchDog锁续约 (下文),一个注册了加锁事件的续约任务。先来看有过期时间 tryLockInnerAsync 部分

  • evalWriteAsync 是 eval 命令执行lua脚本的入口

在这里插入图片描述

-- 如果不存在key时
if (redis.call('exists', KEYS[1]) == 0) then-- 新增该锁并且hash中该线程id对应的count置1redis.call('hincrby', KEYS[1], ARGV[2], 1);-- 设置过期时间redis.call('pexpire', KEYS[1], ARGV[1]);return nil; 
end;
-- 存在该key并且hash中线程id的key也存在
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]);

和前面写自定义的分布式锁的脚本几乎一致,看来redisson也是一样的实现,具体参数分析

// keyName
KEYS[1] = Collections.singletonList(this.getName())
// leaseTime
ARGV[1] = this.internalLockLeaseTime
// uuid + threadId组合的唯一值
ARGV[2] = this.getLockName(threadId)

总共3个参数完成了一段逻辑

判断该锁是否已经有对应hash表存在,• 没有对应的hash表: 则set该hash表中一个entry的key为锁名称,value为1,之后设置该hash表失效时间为leaseTime• 存在对应的hash表: 则将该lockName的value执行+1操作,也就是计算进入次数,再设置失效时间leaseTime• 最后返回这把锁的ttl剩余时间

RLock 如何解锁?

  • 对应的,有key的对应hashKey当中的value+1,就有key的对应hashKey当中的value-1,代码如下

在这里插入图片描述

-- 不存在key
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) thenreturn nil;
end;
-- 计数器 -1
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;

该 lua KEYS有2个Arrays.asList(getName(), getChannelName()),ARGV变量有三个LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)

// 锁名称
KEYS[1] = this.getName()
// 用于pub/sub发布消息的channel名称
KEYS[2] = this.getChannelName()
// channel发送消息的类别,此处解锁为0
ARGV[1] = LockPubSub.UNLOCK_MESSAGE
// watchDog配置的超时时间,默认为30s
ARGV[2] = this.internalLockLeaseTime
// 这里的lockName指的是uuid和threadId组合的唯一值
ARGV[3] = this.getLockName(threadId)

释放锁过程如下

1.如果该锁不存在则返回nil2.如果该锁存在则将其线程的hash key计数器-13.计数器counter>0,重置下失效时间,返回0;否则,删除该锁,发布解锁消息unlockMessage,返回1

其中unLock的时候使用到了Redis发布订阅Pub / Sub完成消息通知

而订阅的步骤就在RedissonLock的加锁入口的tryLock方法里

Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);if (ttl == null) {return true;} else {time -= System.currentTimeMillis() - current;if (time <= 0L) {this.acquireFailed(waitTime, unit, threadId);return false;} else {current = System.currentTimeMillis();RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {if (!subscribeFuture.cancel(false)) {subscribeFuture.onComplete((res, e) -> {if (e == null) {this.unsubscribe(subscribeFuture, threadId);}});}// 省略...

当锁被其他线程占用时,通过监听锁的释放通知(在其他线程通过RedissonLock释放锁时,会通过发布订阅pub/sub功能发起通知),等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段

3.2 消息发布与订阅

发布消息

一探究竟通知了什么,通知后又做了什么,进入LockPubSub

org.redisson.pubsub.PublishSubscribe#subscribe方法下

在这里插入图片描述

org.redisson.pubsub.PublishSubscribe#createListener

在这里插入图片描述

    @Overrideprotected void onMessage(RedissonLockEntry value, Long message) {// 解锁消息 if (message.equals(UNLOCK_MESSAGE)) {// 从监听器队列取监听线程执行监听回调Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute != null) {runnableToExecute.run();}// getLatch()返回的是Semaphore,信号量,此处是释放信号量// 释放信号量后会唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁value.getLatch().release();} else if (message.equals(READ_UNLOCK_MESSAGE)) {	// 读锁解锁消息while (true) {Runnable runnableToExecute = value.getListeners().poll();if (runnableToExecute == null) {break;}runnableToExecute.run();}value.getLatch().release(value.getLatch().getQueueLength());}}

发现一个是默认解锁消息 ,一个是读锁解锁消息 ,因为redisson是有提供读写锁的,而读写锁读读情况和读写、写写情况互斥情况不同,我们只看上面的默认解锁消息unlockMessage分支

LockPubSub 监听最终执行了2件事

  • runnableToExecute.run() 执行监听回调
  • value.getLatch().release() 释放信号量

Redisson通过 LockPubSub 监听解锁消息,执行监听回调和释放信号量通知等待线程可以重新抢锁

3.3 Watch Dog(看门狗机制)

这时再回来看tryAcquireOnceAsync另一分支

在这里插入图片描述

可以看到,无超时时间时,在执行加锁操作后,还执行了一段费解的逻辑

RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {if (e != null) {return;}// lock acquiredif (ttlRemaining == null) {scheduleExpirationRenewal(threadId);}
});

此处涉及到Netty的 Future/Promise-Listener 模型,Redisson 中几乎全部以这种方式通信(所以说 Redisson 是基于 Netty 通信机制实现的),理解这段逻辑可以试着先理解下面这些内容

在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run()执行完毕意味着业务逻辑的完结,在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑

这块代码的表面意义就是,在执行异步加锁的操作后,加锁成功则根据加锁完成返回的 ttl 是否过期来确认是否执行一段定时任务

这段定时任务的就是 watchDog 的核心

3.4 锁续约

查看org.redisson.RedissonBaseLock#scheduleExpirationRenewal(long threadId)

protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);// 重入加锁if (oldEntry != null) {oldEntry.addThreadId(threadId);} else {// 第一次加锁,触发定时任务entry.addThreadId(threadId);renewExpiration();}
}private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}// 开启一个定时任务、每30 / 3秒执行一次Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}RFuture<Boolean> future = renewExpirationAsync(threadId);	// 异步续锁时间future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// reschedule itselfrenewExpiration();	// 回调定时任务}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}

拆分来看,这段连续嵌套且冗长的代码实际上做了几步

• 添加一个netty的Timeout回调任务,每(internalLockLeaseTime / 3)毫秒执行一次,执行的方法是renewExpirationAsync(long threadId)• renewExpirationAsync重置了锁超时时间,又注册一个监听器,监听回调又执行了renewExpiration

renewExpirationAsync 的 lua 如下

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('pexpire', KEYS[1], ARGV[1]);return 1;
end;
return 0;KEYS[1] = Collections.singletonList(getName());
ARGV[1] = internalLockLeaseTime;	// 默认是30s
ARGV[2] = getLockName(threadId);

重新设置了超时时间

Redisson加这段逻辑的目的是什么?

目的是为了某种场景下保证业务不影响,如任务执行超时但未结束,锁已经释放的问题

当一个线程持有了一把锁,由于并未设置超时时间 leaseTime,Redisson 默认配置了30S,开启 watchDog,每10S对该锁进行一次续约,维持30S的超时时间,直到任务完成再删除锁

在这里插入图片描述

这就是Redisson的锁续约 ,也就是WatchDog 实现的基本思路

流程概括

- A、B线程争抢一把锁,A获取到后,B阻塞
- B线程阻塞时并非主动CAS,而是PubSub方式订阅该锁的广播消息
- A操作完成释放了锁,B线程收到订阅消息通知
- B被唤醒开始继续抢锁,拿到锁

在这里插入图片描述

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

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

相关文章

【数据分析之道①】字符串

文章目录专栏导读1、字符串介绍2、访问字符串中的值3、字符串拼接4、转义字符5、字符串运算符6、字符串格式化7、字符串内置函数专栏导读 ✍ 作者简介&#xff1a;i阿极&#xff0c;CSDN Python领域新星创作者&#xff0c;专注于分享python领域知识。 ✍ 本文录入于《数据分析之…

SpringCloud:初识RabbitMQ及快速入门

1.初识MQ 1.1.同步和异步通讯 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c;打电话可以立即得到响应&#xff0c;但…

每个开发人员都需要掌握的10 个基本 SQL 命令

SQL 是一种非常常见但功能强大的工具&#xff0c;它可以帮助从任何数据库中提取、转换和加载数据。数据查询的本质在于SQL。随着公司和组织发现自己处理的数据量迅速增加&#xff0c;开发人员越来越需要有效地使用数据库来处理这些数据。所以想要暗恋数据领域&#xff0c;SQL是…

数据挖掘(作业汇总)

目录 环境配置 实验1 数据 作业2 环境配置 实验开始前先配置环境 以实验室2023安装的版本为例&#xff1a; 1、安装anaconda&#xff1a;&#xff08;anaconda自带Python,安装了anaconda就不用再安装Python了&#xff09; 下载并安装 Anaconda3-2022.10-Windows-x86_64.ex…

分片压缩、分片上传,融云 IM 视频文件高速传输方案

在 IM 消息管理中&#xff0c;多种类型消息的传输处理是服务可靠性的关键。关注【融云全球互联网通信云】了解更多 通常&#xff0c;发送消息前&#xff0c;融云 IM 会将发送的媒体文件上传到默认文件服务器。 而在文本、表情、图片、语音、位置、小视频等各种消息中&#xf…

unity+vs code+mac环境安装配置

参考资料&#xff1a;unity官方文档&#xff1a;https://docs.unity3d.com/cn/current/Manual/ScriptingToolsIDEs.html安装unity1、打开unity中国官网下载&#xff0c;https://unity.cn/releases#undefined2、安装成功后&#xff0c;登录帐号。3、安装unity 推荐版本mac 配置C…

Matlab中对三维图进行视角观察设置——相机视线函数view

Matlab中对三维图进行视角观察设置——相机视线函数view1.view函数的功能&#xff1a;相机视线&#xff1b;2.view函数的调用语法&#xff1a;当我们采用matlab中的surf函数等绘制好三维图像后&#xff0c;想观察某个角度的图像时&#xff0c;可采用view函数快速多角度便捷设置…

Hive数据仓库简介

文章目录Hive数据仓库简介一、数据仓库简介1. 什么是数据仓库2. 数据仓库的结构2.1 数据源2.2 数据存储与管理2.3 OLAP服务器2.4 前端工具3. 数据仓库的数据模型3.1 星状模型3.2 雪花模型二、Hive简介1. 什么是Hive2. Hive的发展历程3. Hive的本质4. Hive的优缺点4.1 优点4.2 缺…

8个python自动化脚本提高打工人幸福感~比心~

人生苦短&#xff0c;我用Python 最近有许多打工人都找我说打工好难 每天都是执行许多重复的任务&#xff0c; 例如阅读新闻、发邮件、查看天气、打开书签、清理文件夹等等&#xff0c; 使用自动化脚本&#xff0c;就无需手动一次又一次地完成这些任务&#xff0c; 非常方便…

2023北京/杭州/湖南/广东CDGA/CDGP数据治理工程师认证报名

DAMA认证为数据管理专业人士提供职业目标晋升规划&#xff0c;彰显了职业发展里程碑及发展阶梯定义&#xff0c;帮助数据管理从业人士获得企业数字化转型战略下的必备职业能力&#xff0c;促进开展工作实践应用及实际问题解决&#xff0c;形成企业所需的新数字经济下的核心职业…

《Spring Boot 趣味实战课》读书笔记(四)

你有 REST Style 吗 你应该懂一点 HTTP HTTP 就是 HyperText Transfer Protocol&#xff08;超文本传输协议&#xff09;的缩写。 它是一种关于“传输”的协议&#xff0c;既然是传输&#xff0c;那么至少要在两个对象之间进行&#xff0c;在 HTTP 中对应的就是客户端和服务端…

KCon 2023兵器谱招募开启!以「兵器」会友,热血相聚!

2023第十二届 KCon大会已启动&#xff0c;议题招募正在火热进行中。&#xff08;点击查看&#xff09;与此同时&#xff0c;兵器谱召集也正式开启&#xff01;我们现诚邀众安全研究员踊跃展示「神兵利器」&#xff0c;以「兵器」会友&#xff0c;逐鹿网络江湖。 「器」既为容纳…

联合体在内存中的分布情况,大小端的判断方法

一、联合体的定义 联合是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员&#xff0c;特征是这些成员公用同一块空间&#xff0c;所以联合体也叫共用体。 //联合类型的声明 union Un { char c; int i; }; int main() { //联合变量的定义 union U…

项目二 任务三 训练5 交换机的HSRP技术

在“项目二 任务三 训练4 交换机的DHCP技术”基础上继续完成下列操作&#xff1a; 1、二层交换机50-2的配置 50-2>en 50-2#conf t Enter configuration commands, one per line. End with CNTL/Z. 50-2(config)#int 50-2(config)#interface g 50-2(config)#interface gigab…

什么是队列,如何实现?

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; “海色温柔&#xff0c;波浪缓慢&#xff0c;似乎在静静期待着新的一天。” 前言&#xff1a; 上期我们讲了栈&#xff0c;它的特点是“后入先出”。这次我们再来学习一个新的数据结构&#xff1a;队列&…

索尼mp4变成rsv的修复方法

索尼的摄像机在一些极端情况下(如断电)会生成RSV文件&#xff0c;遇到这种情况我们应该如何处理&#xff1f;下面来看看今天这个案例。故障文件:22.4G RSV文件故障现象:断电后仅生成了一个扩展名为rsv的文件&#xff0c;无法播放。故障分析:经过长时间处理索尼的摄像机&#xf…

WSO2 Apim Message Mediation (Api Policies)

WSO2 Apim Message Mediation 1. 版本区别2. 客制化2.1 Wso2 Integration Studio Install2.2 New Sequence2.3 测试

Element table组件内容\n换行解决办法

项目使用<el-table>组件 <el-table :data"warnings" :row-class-name"highlightRow" v-loading"isLoading"> <el-table-column label"ID" prop"id"/> <el-table-column label"时间" pro…

VS2022 webapi SQLite EFcore 最简单部署

一、我有一个sqlite单文件数据库&#xff0c;里面有一张表material&#xff0c;我想把这张表的数据&#xff0c;让c# webapi程序从服务器上输出成json,让客户端可以查询到数据。二、使用VS2022&#xff0c;安装ASP.net相关开发组件。三、VS2022中新建一个项目&#xff0c;项目的…

VMvare-linux没有图形化界面

镜像&#xff1a; linux centos7.5 软件&#xff1a;vmware 安装过程&#xff1a;基本一路默认 问题&#xff1a;安装成功后&#xff0c;只有命令行&#xff0c;没有图形界面 解决办法如下&#xff1a; 1、检测yum是否可以使用 yum list | tail2、开始安装 yum groupins…