Redis-分布式锁
Redis-分布式锁
文章目录
- Redis-分布式锁
- 1.基本原理和不同方式实现方式对比
- 2.Redis分布式锁的基本实现思路
- 3.分布式锁误删问题一
- 4.分布式锁误删问题二
- 5.Redission
- 1.功能介绍
- 2.快速入门
- 3.可重入锁原理
- 4.锁重试和WatchDog机制
- 1.锁重试
- 2. WatchDog 机制(锁自动续期)
- 3.锁重试的获取与释放锁流程图
- 5.MultiLock原理(联合锁)
这篇文章帮助大家简单了解利用Redis实现分布锁,并在实现过程中逐步完善在并发时会出现的问题。
1.基本原理和不同方式实现方式对比
如图在单个jvm中,维护了自己的锁监视器,来监视当前jvm内部线程锁的获取情况,保证线程的正确执行。当集群形式时,一个新的tomcat意味着一个新的jvm(也维护了自己的锁监视器),这时对于jvm1来说对于用户id为1(假设)下单,可以获取到锁,jvm2中用户id为1下单也可以获取到锁,此时无法实现一人一单,存在线程安全问题。
为了解决这个问题,我们可以维护一个外部的锁监视器,让外部的锁监视器来管理多个集群的锁,实现多线程互斥。
所以什么是分布式锁?
分布式锁:满足分布式系统或者集群模式下多进程课件并且互斥的锁。
注意分布式锁的核心是实现多进程之间互斥,满足这一点的方式有很多种,常见的有三种:
以下是提取后的分布式锁三种实现方式的对比表格:
特性 | MySQL | Redis | Zookeeper |
---|---|---|---|
互斥 | 利用 MySQL 本身的互斥锁机制 | 利用 setnx 等互斥命令 | 利用节点的唯一性和有序性实现互斥 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 好 | 一般 |
安全性 | 断开连接后自动释放锁 | 通过锁超时时间到期释放 | 临时节点,断开连接后自动释放 |
有关说明
-
互斥机制
- MySQL:基于行锁或乐观锁实现。
- Redis:通过
SETNX
(或SET key value NX EX
)实现原子性抢锁。 - Zookeeper:利用临时有序节点的唯一性(如最小节点获锁)。
-
安全性
- MySQL/Redis 需显式处理锁释放(如超时或断开连接)。
- Zookeeper 的临时节点在会话终止时会自动删除,避免死锁。
-
适用场景
- Redis:高性能、短期锁场景(如秒杀)。
- Zookeeper:强一致性、长期锁场景(如选主)。
- MySQL:依赖数据库且对性能要求不高的场景。
2.Redis分布式锁的基本实现思路
实现分布式锁时需要实现的两个基本方法:
-
获取锁:
-
互斥:确保只能有一个线程获取锁
-
非阻塞:尝试获取锁一次,成功返回true,否则返回false
#添加锁,利用setnx的互斥特性 SETNX lock thread1 #添加锁过期时间,避免服务宕机引起的死锁 EXPIRE lock 10
注意:这里可能存在添加锁后,还没来得及添加锁的过期时间,服务宕机此时锁可能就会无法释放,因此应该保证两个操作的原子性。
#添加锁并添加过期时间NX是互斥,EX是设置超时时间 SET lock thread1 NX EX 10
-
-
释放锁:
-
手动释放
-
超时释放:获取锁时添加一个超时时间
#释放锁,删除即可 DEL key
-
基于上述我们获取锁和释放锁的流程如图所示:
基于redis实现分布式锁初级版本:
需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。
//接口
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功;false代表获取锁失败
*/
boolean tryLock(long timeoutSec);/**
* 释放锁
*/
void unlock();
}
//实现类
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{// 锁的统一前缀private static final String key_prefix = "lock:";//锁的业务名称private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec) {//使用当前线程的id作为锁的值String lockValue = String.valueOf(Thread.currentThread().getId());//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key_prefix + name, lockValue, timeoutSec, TimeUnit.SECONDS);//直接返回自动拆箱可能会出现空指针异常/* return success;*/return Boolean.TRUE.equals(success);//null会转为false}@Overridepublic void unlock() {//解锁stringRedisTemplate.delete(key_prefix + name);}
}
3.分布式锁误删问题一
如图线程一获取锁后,因为业务阻塞时间过长,锁超时释放。
线程二乘虚而入获取到锁并执行业务,但是线程一业务完成并删除锁(注意删除的是线程二的锁)。
此时线程三也获取到锁,出现线程二、三并行执行,线程安全问题可能发生。
解决方式:当要去释放锁时,检查此锁是不是自己的,再进行操作,这样就不会误删别人的锁。
改进措施:
-
在获取锁时存入线程标示(用UUID表示)(因为每一个jvm维护线程的名称是是id自增,可能存在相同的情况)
-
在释放锁时先获取锁中的线程标示,判断是否与当前线程标示:
-
一致则释放锁
-
不一致则不释放锁
-
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{// 锁的统一前缀private static final String KEY_PREFIX = "lock:";//--------------------------------------------------------------------------------------//线程id保证唯一性private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";//--------------------------------------------------------------------------------------//锁的业务名称private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec) {//使用当前线程的id作为锁的值String lockValue = ID_PREFIX+Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, lockValue, timeoutSec, TimeUnit.SECONDS);//直接返回自动拆箱可能会出现空指针异常/* return success;*/return Boolean.TRUE.equals(success);//null会转为false}@Overridepublic void unlock() {//获取当前锁占有的值(值是线程id)String ID = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);//线程的idString threadId = String.valueOf(Thread.currentThread().getId());//判断是不是当前线程占有的锁//是if(ID.equals(threadId)){//解锁stringRedisTemplate.delete(KEY_PREFIX + name);}}
}
此时获取锁和释放锁的流程如图所示:
4.分布式锁误删问题二
我们当前实现的锁在一些情况下还是会出现问题:
如图线程一获取锁并执行业务,业务执行完毕判断当前锁是自己的,但是在删除所之前,发生阻塞。
虽然判断和和释放锁之间没有代码,但是在jvm中存在垃圾回收机制,这里不是业务阻塞而可能是jvm阻塞。
其他进程进入并获取到锁,线程一的阻塞结束并执行释放锁操作(已经判断过),此时删除的是其他线程的锁,则又变成问题一。
这个问题发生的原因是判断锁标志和释放锁是两个动作,在两个动作之间产生阻塞,我们需要保证这两个动作的原子性。
这个问题使用Redis的Lua脚本解决:
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
这里简单介绍下Redis在Lua中提供的调用函数,语法如下:
#执行 redis命令
redis.call('命令名称', 'key', '其它参数', ...)
#例如,我们要执行set name jack,则脚本是这样:
#执行 set name jackredis.call('set', 'name', 'jack')
#例如,我们要先执行set name Rose,再执行get name,则脚本如下:
#先执行 set name jack
redis.call('set', 'name', 'jack')#再执行 get name
local name = redis.call('get', 'name')#返回
return name
写好脚本以后,用命令行中使用Redis命令来调用脚本,调用脚本的常见命令如下:
#例如,我们要执行 redis.call('set', 'name', 'jack') 这个脚本,语法如下:
#调用脚本
EVAL "return redis.call('set', 'name', 'jack')" 0# 脚本内容 脚本需要的key类型的参数个数
如果脚本中的key、value不想写死,可以作为采纳数传递。key类型采纳数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
现在我们来改进redis的分布锁:
注意在Java中使用RedisTemplate调用Lua脚本的API如下:
-
在resources下创建并编写Lua脚本
--锁的key local key =KEY[1] --当前线程标示 local threadId =ARGV[1] --获取锁种的线程标示 local id =redis.call('get',key) --比较线程标示和锁种的标示是否一致 if(id == threadId) then--释放锁 del keyreturn redis.call('del',key) end return 0
-
重新实现两个方法
import cn.hutool.core.lang.UUID; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import java.util.Collections; import java.util.concurrent.TimeUnit; public class SimpleRedisLock implements ILock{// 锁的统一前缀private static final String KEY_PREFIX = "lock:";//线程id保证唯一性private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";//锁的业务名称private String name;private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}//--------------------------------------------------------------------------private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;/*作用:定义一个静态不可变的 DefaultRedisScript 对象,用于加载和执行 Lua 脚本。泛型 <Long>:指定脚本返回值的类型为 Long (Lua 脚本返回的数字类型在 Java 中映射为 Long)。*/static{UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}/*new DefaultRedisScript<>():创建脚本执行器实例。setLocation():从类路径(resources 目录)加载 Lua 脚本文件 unlock.lua。setResultType():声明脚本返回值的类型为 Long(例如返回 1 表示成功,0 表示失败)*///--------------------------------------------------------------------------@Overridepublic boolean tryLock(long timeoutSec) {//使用当前线程的id作为锁的值String lockValue = ID_PREFIX+Thread.currentThread().getId();//获取锁Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, lockValue, timeoutSec, TimeUnit.SECONDS);//直接返回自动拆箱可能会出现空指针异常/* return success;*/return Boolean.TRUE.equals(success);//null会转为false}@Overridepublic void unlock() {stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX+name),ID_PREFIX+Thread.currentThread().getId());} }
此时我们就已经实现了一个生产可用的基于redis的分布式锁。
5.Redission
基于setnx实现的分布式锁还存在以下问题:
- 不可重入:同一个线程无法多次获取同一把锁。
- 不可重试:获取锁只尝试一次就返回false,没有重试机制。
- 超时释放:锁超时释放虽然可以避免死锁,但如果业务的执行耗时较长,也会导致锁释放,存在安全隐患,也就是锁的生存时长不确定。
- 主从一致性:如果Redis配置为主从集群,由于主从同步存在延迟,当主节点宕机时,若从节点尚未完全同步主节点中的锁数据,则会出现锁失效的问题。
通过之前的介绍我们已将了解了分布式锁的基本原理,不过为了解决这些问题我们需要使用一些成熟的框架来帮我们实现------Redission
1.功能介绍
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。
例如:
分布式锁(Lock)和同步器(Synchronizer)
- 可重入锁(Reentrant Lock)
- 公平锁(Fair Lock)
- 联锁(MultiLock)
- 红锁(RedLock)
- 读写锁(ReadWriteLock)
- 信号量(Semaphore)
- 可过期性信号量(PermitExpirableSemaphore)
- 闭锁(CountDownLatch)
官网地址:https://redisson.org
GitHub地址:https://github.com/redisson/redisson
2.快速入门
-
引入依赖
//引入redission依赖 <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.6</version> </dependency>
-
配置Redission客户端
//配置Redission客户端 @Configuration public class RedisConfig {@Beanpublic RedissonClient redissonClient() {// 配置类Config config = new Config();// 添加redis地址,这里添加了单点的地址,//也可以使用config.useClusterServers()添加集群地址config.useSingleServer().setAddress("redis://地址:6379").setPassword("密码");// 创建客户端return Redisson.create(config);} }
-
使用Redission的分布式锁
@Resource private RedissonClient redissonClient; @Test void testRedisson() throws InterruptedException {// 获取锁(可重入),指定锁的名称RLock lock = redissonClient.getLock("anylock");// 尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);// 判断是否获取成功if(isLock){try {System.out.println("执行业务");}finally {// 释放锁lock.unlock();}} }
3.可重入锁原理
我们之前锁的实现是基于redis中String类型key-value形式,当第一次获取锁成功后,同一个线程再次获取锁,则会判断锁已经被获取从而获取失败。
如图为我们解决锁误删问题所实现的锁的流程图。
可重入锁是基于Hash结构实现的,当我们获取锁后会存入表示和初始化value为1,并添加锁的过期时间。
当同一个线程再次获取当前锁时令value值++,完成一个业务时会释放锁令**值value–**并重置锁的过期时间,直到value值减为0,才会真正释放锁。
如图为可重入锁的流程图逻辑。
为了保证加锁和释放锁过程的一致性,Redission的底层也使用了lua脚本来实现。(注意这里并不是redission中的lua源码)
获取锁的lua脚本:
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断是否存在
if(redis.call('exists', key) == 0) then-- 不存在,获取锁redis.call('hset', key, threadId, '1');-- 设置有效期redis.call('expire', key, releaseTime);return 1; -- 返回结果
end;
-- 锁已经存在,判断threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then-- 存在,获取锁,重入次数+1redis.call('hincrby', key, threadId, '1');-- 设置有效期redis.call('expire', key, releaseTime);return 1; -- 返回结果
end;
return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
释放锁的lua脚本:
local key = KEYS[1]; -- 锁的key
local threadId = ARGV[1]; -- 线程唯一标识
local releaseTime = ARGV[2]; -- 锁的自动释放时间
-- 判断当前锁是否还是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then return nil; -- 如果已经不是自己,则直接返回
end;
-- 是自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 判断重入次数是否已经为0
if (count > 0) then-- 大于0说明不能释放锁,重置有效期然后返回redis.call('EXPIRE', key, releaseTime);return nil;
else -- 等于0说明可以释放锁,直接删除redis.call('DEL', key);return nil;
end;
如下图为redisson中一段实现获取锁的代码,其利用lua脚本硬编码实现。(大家可自行查看源码学习)
4.锁重试和WatchDog机制
1.锁重试
tryLock()
方法是 Redisson 提供的可等待的分布式锁,它有多个重载版本,最常用的三个参数版本是:
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
这三个参数决定了锁的获取行为、持有时间,并直接影响 WatchDog 是否生效。
1. tryLock() 的三个参数
(1)waitTime
:获取锁的最大等待时间
-
作用:在这段时间内,如果锁被其他线程占用,当前线程会不断尝试获取锁
不是无脑的无休止尝试获取锁,而是基于 Redis 的 Pub/Sub 消息订阅机制 监听锁释放消息,而这个释放锁的通知就来自于lua脚本中的publish命令:
-
示例:
lock.tryLock(10, TimeUnit.SECONDS); // 最多等10秒,拿不到就放弃
-
底层实现:
- 先尝试立即获取锁。
- 如果失败,则订阅 Redis 的锁释放事件(避免 CPU 空转)。
- 在
waitTime
内,如果收到锁释放的通知,就再次尝试获取。 - 如果超时仍未获取到,返回
false
。
(2)leaseTime
:锁的自动释放时间
-
作用:设置锁的最大持有时间,防止死锁(即使业务代码未显式释放,锁也会自动过期)。
注意如果不设置leaseTime,则会使用默认值-1。
-
示例:
lock.tryLock(1, 10, TimeUnit.SECONDS); // 此锁最多持有10秒
2. WatchDog 机制(锁自动续期)
(1)什么情况下会触发 WatchDog?
- 未指定
leaseTime
(或leaseTime = -1
)时,Redisson 会启动 WatchDog。 - 示例:
lock.tryLock(10, -1, TimeUnit.SECONDS); // 启用 WatchDog
(2)WatchDog 的作用
- 防止业务执行时间过长导致锁提前释放:
- 默认情况下,WatchDog 每隔 10 秒 检查一次锁是否仍然被当前线程持有。
- 如果是,则自动续期 30 秒(默认
lockWatchdogTimeout=30s
)。
- 示例流程:
- 线程 A 获取锁,未指定
leaseTime
,WatchDog 启动。 - 每 10 秒检查一次:
- 如果锁仍被线程 A 持有,则重置 TTL 为 30 秒。
- 如果锁已释放,则停止 WatchDog。
- 业务执行完毕后,手动
unlock()
,WatchDog 停止。
- 线程 A 获取锁,未指定
3.锁重试的获取与释放锁流程图
简单总结:
- 可重试机制
- 实际使用 Redis 的 Pub/Sub 监听锁释放通知。
- 获取失败后订阅通知,收到释放通知后重试,避免忙等待。
- 重试必须在waitTime之内
- WatchDog 续约逻辑
- 默认
releaseTime
(即lockWatchdogTimeout
)为 30秒,续约间隔为 10秒(30/3)。 - 仅在未显式设置
leaseTime
时生效 - watchdog是在当显示的使用unlock释放锁或持有锁的jvm线程崩溃时关闭。
- 默认
5.MultiLock原理(联合锁)
如图为主从同步时会发生的问题,当客户端向主节点成功获取锁后,主从准备同步数据时,主机宕机导致锁数据失效,此时会出现并发问题。
为了解决这个问题,第一个想法是取消主从机制,转而改为多节点的主机,此时我们需要向每个节点获取锁,都保存锁信息才算获取成功。
如果还想使其的可用性更高,可以给每个节点建立主从机制。此时当一个节点宕机时便不会出现主从数据同步导致的锁数据丢失问题。
当第一个节点宕机,此时是可以在其子节点中拿到锁,但是在其他两个节点无法获取到锁。也就是只要有一个节点存活,就无法获取到锁,则不会出现锁失效问题。这种方案保留了主从同步机制,保证了集群的稳定性和高可用性,解决了主从同步的锁失效问题,这种方案在Redis中叫做MultiLock原理。
注意multilock根据自己的需求来决定是否使用主从机制,不使用则为刚刚提到的多节点主机方式。
这里简单演示使用过程:
-
配置三个可用的redis客户端
@Configuration public class RedissionConfig {@Beanpublic RedissonClient redissionClient(){//配置redisson客户端Config config = new Config();//useSingleServer()单机模式config.useSingleServer().setAddress("redis://地址").setPassword("密码");//返回配置return Redisson.create(config);}@Beanpublic RedissonClient redissionClient2(){//配置redisson客户端Config config = new Config();//useSingleServer()单机模式config.useSingleServer().setAddress("redis://地址").setPassword("密码");//返回配置return Redisson.create(config);}@Beanpublic RedissonClient redissionClient3(){//配置redisson客户端Config config = new Config();//useSingleServer()单机模式config.useSingleServer().setAddress("redis://地址").setPassword("密码");//返回配置return Redisson.create(config);} }
-
使用redission中的multilock
@Slf4j @SpringBootTest class RedissonTest {@Resourceprivate RedissonClient redissonClient;private RLock lock;@BeforeEachvoid setUp() {RLock lock1 = redissonClient.getLock("order");RLock lock2 = redissonClient.getLock("order");RLock lock3 = redissonClient.getLock("order");//创建连锁lock=redissonClient.getMultiLock(lock1, lock2, lock3);}@Testvoid method1() throws InterruptedException {// 尝试获取锁boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);if (!isLock) {log.error("获取锁失败 .... 1");return;}try {log.info("获取锁成功 .... 1");method2();log.info("开始执行业务 ... 1");} finally {log.warn("准备释放锁 .... 1");lock.unlock();}}void method2() {// 尝试获取锁boolean isLock = lock.tryLock();if (!isLock) {log.error("获取锁失败 .... 2");return;}try {log.info("获取锁成功 .... 2");log.info("开始执行业务 ... 2");} finally {log.warn("准备释放锁 .... 2");lock.unlock();}} }
此时大家可以直接使用提供的演示代码测试来观察获取锁释放锁的过程,如果想要更加了解底层代码,可以进入trylock的multilock实现学习,这里不过多赘述。
到这里相信大家已经对基于redis所实现的分布式锁的原理有了不错的了解,觉得不错的话点个关注,我们一起学习!
相关文章:
Redis-分布式锁
Redis-分布式锁 文章目录 Redis-分布式锁1.基本原理和不同方式实现方式对比2.Redis分布式锁的基本实现思路3.分布式锁误删问题一4.分布式锁误删问题二5.Redission1.功能介绍2.快速入门3.可重入锁原理4.锁重试和WatchDog机制1.锁重试2. WatchDog 机制(锁自动续期&…...
如何优雅地为 Axios 配置失败重试与最大尝试次数
在 Vue 3 中,除了使用自定义的 useRequest 钩子函数外,还可以通过 axios 的拦截器 或 axios-retry 插件实现接口请求失败后的重试逻辑。以下是两种具体方案的实现方式: 方案一:使用 axios 拦截器实现重试 实现步骤: 通…...
Windows使用SonarQube时启动脚本自动关闭
一、解决的问题 Windows使用SonarQube时启动脚本自动关闭,并发生报错: ERROR: Elasticsearch did not exit normally - check the logs at E:\Inori_Code\Year3\SE\sonarqube-25.2.0.102705\sonarqube-25.2.0.102705\logs\sonarqube.log ERROR: Elastic…...
MYSQL初阶(暂为自用草稿)
目录 基本操作 database操作 table操作 数据类型 INT类型 bit类型 FLOAT类型 CHAR类型 DATE类型 SEL类型 表的约束 列约束 NULL DEFAULT PRIMARY KEY UNIQUE KEY 表约束 PRIMARY KEY FOREIGN KEY 其他补充 AUTO_INCREMENT COMMENT ZEROFILL 表的CRUD …...
交换排序——快速排序
交换排序的基本思路:把序列中的两个元素进行比较,根据需求对两个元素进行交换。特点是较大的元素向序列的尾部移动,较小的元素向序列的前部移动。 hoare法 在序列中任取一个元素作为基准值,一趟排序完成之后,以基准值为…...
资源-又在网上淘到金了
前言: 本期再分享网上冲浪发现的特效/动画/视频资源网站。 一、基本介绍: mantissa.xyz,about作者介绍为:Midge “Mantissa” Sinnaeve (米奇辛纳夫)是一位屡获殊荣的艺术家和导演,提供动画、…...
CSS中的`transform-style`属性:3D变换的秘密武器
在CSS中,当我们尝试创建复杂的3D场景时,transform-style属性变得尤为重要。它决定了子元素是在3D空间中呈现还是被展平到2D平面中。本文将深入探讨transform-style的用法,并通过具体的代码示例来展示如何利用这个属性来增强你的网页设计。 什…...
Step文件无法编辑怎么办?
Step文件无法编辑怎么办? 这里介绍两种方法, 1、 直接导入 准备step文件,solidworks导入后是这样,不能在上面直接编辑 图 1 点击右键,选择解除特征(不同版本的可能不太一样,这里是solidworks2…...
从 LabelImg 到 Label Studio!AI 数据标注神器升级,Web 版真香
视频讲解: 从 LabelImg 到 Label Studio!AI 数据标注神器升级,Web 版真香 Label Studio 支持图像、文本、音频、视频、时间序列等多类型数据标注,覆盖计算机视觉(目标检测、语义分割)、自然语言处理&#x…...
纯FPGA实现驱动AD9361配置的思路和实现之一 概述
我们在做ZYNQ系统开发时候做的IP基本都是AXI_LITE_SLAVE,是SLAVE,从设备。就是提供了若干寄存器接口供MASTER进行读写。SLAVE里面的逻辑通过读写动作或者读写的数据进行响应的动作。这种方式的好处是硬件层面可以访问寄存器,软件层面是可以实…...
Nacos配置中心服务端源码解析
文章目录 概述一、配置持久化到数据库二、发布事件2.1、事件发布者端2.1.1、DefaultPublisher#publish2.1.2、DefaultPublisher#run2.1.3、DefaultPublisher#receiveEvent 2.2、事件订阅者端2.2.1、Subscriber#onEvent2.2.2、ConfigCacheService#dump 总结:Nacos 配…...
SAP系统工艺路线的分配物料出现旧版包材
问题:工艺路线的物料错了 这是3月份技术部发现的问题,10000209这个成品有两个版本的BOM, 在创建新版的工艺路线里,发现分配的物料仍然是旧版的物料. 原因排查: 1 BOM中物料错误? 2 选错了生产版本,选了版本1? 3 生产版本设置中的可选BOM错误? 解决:把可选的BOM…...
JVM虚拟机--JVM的组成
(一)JVM的组成 一、JVM介绍 (1)JVM的作用 我们知道,Java代码要想在计算机中正常运行,就需要经过编译为class二进制字节码文件,而JVM就提供了class二进制字节码的运行环境。 一次编写,到处运行 因为JVM是…...
科学研究:怎么做
科研(科学研究) 是指通过系统化的方法,探索自然、社会或人文领域的未知问题,以发现新知识、验证理论或解决实际问题的活动。它的核心是基于证据的探索与创新,旨在推动人类认知和技术的进步。 科研的核心要…...
PyTorch数据操作基础教程:从张量创建到高级运算
本文通过示例代码全面讲解PyTorch中张量的基本操作,包含创建、运算、广播机制、索引切片等核心功能,并提供完整的代码和输出结果。 1. 张量创建与基本属性 import torch# 创建连续数值张量 x torch.arange(12, dtypetorch.float32) print("原始张…...
微服务治理与可观测性
服务注册与发现 核心功能 服务实例动态变化:实例可能因扩缩容、故障或迁移导致IP变动。服务依赖解耦:调用方无需硬编码服务地址,降低耦合度。负载均衡:自动选择健康实例,提升系统可用性。 核心组件 服务注册中心&am…...
如何对docker镜像存在的gosu安全漏洞进行修复——筑梦之路
这里以mysql的官方镜像为例进行说明,主要流程为: 1. 分析镜像存在的安全漏洞具体是什么 2. 根据分析结果有针对性地进行修复处理 3. 基于当前镜像进行修复安全漏洞并复核验证 # 镜像地址mysql:8.0.42 安全漏洞现状分析 dockerhub网站上获取该镜像的…...
OpenCV 04.19 练习
1. 创建一个 PyQt 应用程序,该应用程序能够: 1.使用 OpenCV 加载一张图像。 2.在 PyQt 的窗口中显示这张图像。 3.提供四个按钮(QPushButton): - 一个用于将图像转换为灰度图 - 一个用于将图像恢复为原始彩色图 - 一个…...
uv:重新定义Python开发效率的下一代工具链
在Python生态系统中,包管理和项目工具链的复杂性一直是开发者面临的一大挑战。从依赖管理、虚拟环境创建到多版本Python切换,传统的工具链(如pip、virtualenv、poetry等)虽然功能强大,但操作繁琐、性能不足的问题长期存在。而uv的出现,以颠覆性的速度和功能集成,为Pytho…...
【Easylive】Gateway模块 bootstrap.yml 解析
【Easylive】项目常见问题解答(自用&持续更新中…) 汇总版 Gateway模块 bootstrap.yml 常规解析 该配置文件定义了 Spring Cloud Gateway 的核心配置,包括 环境配置、服务注册、动态路由规则 等。以下是逐项解析: 1. 基础配…...
Warcraft Logs [Classic] [WCL] Usage Wizard <HTOC>
HTOC(十字军的试炼)副本中各个BOSS的ID如下: 629 - 诺森德野兽 633 - 加拉克苏斯大王 637 - 派系冠军 641 - 瓦格里双子 645 - 阿努巴拉克 encounterID!637 and encounterID!641 encounterID NOT IN (637,641) 伤害 …...
多模态大语言模型arxiv论文略读(二十八)
MM-SAP: A Comprehensive Benchmark for Assessing Self-Awareness of Multimodal Large Language Models in Perception ➡️ 论文标题:MM-SAP: A Comprehensive Benchmark for Assessing Self-Awareness of Multimodal Large Language Models in Perception ➡️…...
JavaScript数据类型简介
在JavaScript中,理解不同的数据类型是掌握这门语言的基础。数据类型决定了变量可以存储什么样的值以及这些值能够执行的操作。JavaScript支持多种数据类型,每种都有其特定的用途和特点。本文将详细介绍JavaScript中的主要数据类型,并提供一些…...
CasualLanguage Model和Seq2Seq模型的区别
**问题1:**Causal Language Modeling 和 Conditional Generation 、Sequence Classification 的区别是什么? 因果语言模型(Causal Language Model): 预测给定文本序列中的下一个字符,一般用于文本生成、补全句子等,模型…...
在Qt和OSG中动态改变部分3D模型数据
要在Qt和OSG环境中导入3D模型并只对部分数据进行动态改变,你可以采用以下方法: 基本实现步骤 加载模型:使用OSG的读取器加载3D模型文件 访问特定部分:识别并获取模型中需要修改的部分 动态修改:在Qt界面或逻辑中设置修改这些部分的机制 更新显示:确保修改后的模型能够实…...
命令update-alternatives
❯ which pip /home/ying/anaconda3/bin/pipying192 ~ [2]> which pip /usr/bin/pip使用update-alternatives对他们进行管理和切换 快捷方式 和 实际路径不可以相同 所以我这边选择了/usr/local/bin目录作为介质存储快捷方式,另外该快捷方式会自己创建我们只需选…...
10.thinkphp的响应
响应输出 响应操作 1. 响应输出,有好几种:包括return、json()和view()等等; 2. 默认输出方式是以html格式输出,如果你发起json请求,则输出json; 3. 而背后是response对象,可以用response()输…...
【技术派后端篇】技术派中的白名单机制:基于Redis的Set实现
在技术派社区中,为了保证文章的质量和社区的良性发展,所有发布的文章都需要经过审核。然而,并非所有作者的文章都需要审核,我们通过白名单机制来优化这一流程。本文将详细介绍技术派中白名单的实现方式,以及如何利用Re…...
Keil A51汇编伪指令
以下是 Keil A51 汇编器支持的常用伪指令 及其详细说明,涵盖代码结构、数据定义、条件编译等关键功能,结合实际应用场景进行分类和示例: 一、程序结构与地址控制 伪指令功能语法示例说明ORG设置代码/数据起始地址ORG 0000H后续代码从指定地址…...
Windows上安装FFmpeg的详细指南
1.下载FFmpeg 访问FFmpeg官方下载页面:https://ffmpeg.org/download.html 点击"Windows builds from gyan.dev"或"Windows builds by BtbN" gyan.dev版本:https://www.gyan.dev/ffmpeg/builds/ BtbN版本:https://githu…...
jmeter利用csv进行参数化和自动断言
1.测试数据 csv测试数据如下(以注册接口为例) 2.jemer参数化csv设置 打开 jmeter,添加好线程组、HTTP信息头管理器、CSV 数据文件设置、注册请求、响应断言、查看结果树 1) CSV 数据文件设置 若 CSV 中数据包含中文,…...
《Android 应用开发基础教程》——第二章:Activity 与生命周期详解
目录 第二章:Activity 与生命周期详解 2.1 什么是 Activity? 作用: 2.2 创建一个 Activity 示例代码 Manifest 注册: 2.3 Activity 生命周期(Life Cycle) 生命周期图解: 2.4 生命周期代…...
[Java]反射、String类补充
目录 1、反射定义 2、用途(了解) 3、反射相关的类 4、Class类(反射机制的起源) 4.1、相关方法 5、反射示例 5.1、获取Class对象 5.2、反射的使用 6、反射优点和缺点 7、String类补充 7.1、创建对象的思考 8、字符串常量池 9、再谈String对象创建 10、intern方法 …...
word表格批量操作——宏
word中所有表格代码 这个是表格的模板代码 Sub 表格通用代码() For i ActiveDocument.Tables.Count To 1 Step -1ActiveDocument.Tables (i) Next End Sub1、根据内容自动调整表格 Sub 表格适用内容() For i ActiveDocument.Tables.Count To 1 Step -1ActiveDocument.Tabl…...
eSTK.me
eSTK.me 调用 USIM 卡的 Applet 实现在 iOS 内自助切换 Profile。写卡仍然需要借助硬件读卡器或者兼容 eSIM 的 Android 手机。支持国行 iPhone。 eSTK.me(固件 v2.x 及以后的版本)基于 ETSI 的 Bearer Independent Protocol (BIP) 协议,使…...
四级英语备考指南
一、引言 大学英语四级考试是对大学生英语综合能力的一次重要检验。无论是为了学业要求,还是提升自身竞争力,顺利通过四级考试都有着重要意义。本文将为大家详细介绍四级英语的备考策略,帮助大家高效备考,取得理想成绩。 二、了…...
Java Web 之 互联网协议 100问
HTTP是什么? HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最广泛的协议之一,用于在客户端(如浏览器)和服务器之间传输数据。它是 Web 通信的基础,支持浏览器访…...
【单倍型理解及计算系列之二】单倍型基本概念以及其与遗传定位中Bin的定义区别
问题:如何理解单倍型,与遗传定位中Bin的定义区别: 简而言之:就是单倍型是基于LD,通常为连锁不平衡(LD)较高的区域形成。但bin是人为划分的,如以固定SNP数量/固定长度设置࿰…...
ArcPy Mapping 模块基础
在地理信息系统(GIS)的应用中,地图制作是一个非常重要的环节。无论是生成用于展示的静态地图,还是动态更新的地图服务,都需要高效且灵活的工具来实现。ArcPy 提供了强大的mapping模块,可以帮助用户自动化地…...
AcWing 11:背包问题求方案数 ← 0-1背包
【题目来源】 https://www.acwing.com/problem/content/11/ 【题目描述】 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总…...
Redis增删改查
### 进入redis控制台 redis-cli --raw #加上raw,防止中文乱码### 增 127.0.0.1:6379> LPUSH list0 "hello" #增加一个list 1 127.0.0.1:6379> LRANGE list0 0 -1 #查看list hello### 删 127.0.0.1:6379> DEL list0 #删除list 1 127.0.0.1:6379> LRANG…...
多道程序和多任务操作系统区别
多道程序 vs. 多道任务:对比分析 ✅ 共同点 方面共同特征核心机制都依赖于进程/任务切换执行需求实现多个程序或任务"并发"执行系统支持都需要操作系统的支持(如调度算法、内存管理)本质目标提高资源利用率(CPU不空转…...
【MySQL】MySQL建立索引不知道注意什么?
基本原则: 1.选择性原则: 选择高选择性的列建立索引(该列有大量不同的值) 2.适度原则:不是越多越好,每个索引都会增加写入开销 列选择注意事项: 1.常用查询条件列:WHERE字句中频繁使用的列 2.连接操作列…...
区块链木材业务服务平台:商贸物流新变革
区块链木材业务服务平台:商贸物流新变革 在全球商贸物流行业不断发展的当下,木材贸易作为其中重要的一环,面临着诸多挑战。区块链木材业务服务平台的出现,为木材商贸物流领域带来了全新的解决方案,正逐步引领行业走向…...
【AI提示词】经济学家
提示说明 经济学家致力于提供深入的经济分析和预测,帮助用户理解经济趋势、政策影响以及市场动态。他们通过专业的经济模型和数据分析,为用户在投资、决策等方面提供指导。 提示词 # 角色 经济学家## 注意 1. 经济学家专家需要具备深入分析经济现象的…...
C++用于保留浮点数的两位小数,使用宏定义方法(可兼容低版本Visual Studio)
文章目录 一、 描述二、 样例二、 结果输出 一、 描述 这个宏定义(可放入.h头文件里)使用基本的数学运算,几乎兼容所有版本的VS,以下可对正数做四舍五入: #define ROUND_TO_TWO(x) ( (floor((x) * 100 0.5) / 100) …...
kimi+deepseek制作PPT
文章目录 KIMI简介一、基本信息二、核心特点三、服务理念 Deepseek简介PPT关键词提示 KIMI简介 KIMI官网:Kimi - 会推理解析,能深度思考的AI助手 一、基本信息 名称 :KIMI开发团队 :月之暗面科技有限公司上线时间 :…...
Linux-进度条小程序
1. 回车和换行的差异 在输出文本时,回车和换行符的作用是非常不同的。了解它们的行为有助于我们控制输出的方式。 回车(\r):回车符将光标移到当前行的开头,但并不会自动换行。它的作用是覆盖当前行的内容。 换行&…...
Day2—3:前端项目uniapp壁纸实战
接下来我们做一个专题精选 <view class"theme"><common-title><template #name>专题精选</template><template #custom><navigator url"" class"more">More</navigator></template></common…...
什么是超类实体和派生属性
在数据库设计(尤其是实体-关系模型(ER模型))和面向对象建模中,超类实体和派生属性是两个重要的概念,分别用于描述实体间的继承关系和属性的动态计算特性。以下是它们的详细解释和对比: 一、超类…...