使用 Redis 实现分布式锁,解决分布式锁原子性、死锁、误删、可重入、自动续期等问题(使用SpringBoot环境实现)
目录
- 一、前言
- 二、分布式锁具备的特点
- 三、Redis分布式锁的实现核心思路
- 四、分布式锁代码实现(解决分布式锁原子性、死锁、误删、可重入、自动续期等问题)
- 4.1、分布式锁实现工具类
- 4.2、测试分布式锁效果
- 五、分布式锁常见问题以及解决方法
- 5.1、分布式锁死锁问题
- 5.1.1、逻辑说明
- 5.1.2、解决方案
- 5.2、分布式锁原子性问题
- 5.2.1、逻辑说明
- 5.2.2、解决方案
- 5.3、分布式锁可重入问题
- 5.3.1、逻辑说明
- 5.3.2、解决方案
- 5.4、分布式锁如何防止误删
- 5.4.1、逻辑说明
- 5.4.2、解决方案
- 5.5、分布式锁自动续期问题
- 5.5.1、逻辑说明
- 5.5.2、解决方案
一、前言
分布式锁是一种用于在分布式系统中实现同步和互斥访问的机制。在分布式系统中,多个节点同时访问共享资源可能会导致数据不一致或竞争条件的发生。分布式锁提供了一种保护共享资源的方式,以确保在任意时刻只有一个节点可以访问该资源,如:同一时刻每个订单只能有一个线程操作取消订单功能。
- 常见分布式锁实现:
- MySQL:MySQL本身就带有锁机制,由于业务特性使用MySQL作为分布式锁并不合适,而且性能一般,一般很少使用MySQL来实现分布式锁。
- ZooKeeper:ZooKeeper是企业级开发中较好的一个实现分布式锁的方案,相对于Redis,ZooKeeper的部署和维护复杂一些。此外,ZooKeeper的性能相对较低,适用于对性能要求不高的场景。
- Redis:Redis分布式锁的实现通常使用了SETNX(SET if Not eXists)命令和EXPIRE命令。使用SETNX可以尝试将一个键值对设置到Redis中,只有在该键不存在的情况下才能成功。成功获取锁的客户端可以设置一个过期时间,确保即使在发生故障的情况下,锁也能自动释放。
二、分布式锁具备的特点
实现的分布式锁,需要具备一下特征:
| 特点 | 描述 |
|---|---|
| 独占性 | 任何时刻有且只有一个线程持有使用该锁 |
| 高可用 & 高性能 | 程序不易崩溃,时刻都保证较高的可用性,在redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况;在高并发请求下,分布式锁依旧具有良好的性能; |
| 防死锁 | 不能出现死锁问题,必须有超时重试机制或者撤销操作,有个终止跳出的途径; |
| 不乱抢 | 多线程下,防止张冠李戴,只能解锁自己的锁,不能把别人的锁给释放了; |
| 重入性 | 同一节点的同一线程如果获得锁之后,该线程可以再次获取使用这个锁 |
三、Redis分布式锁的实现核心思路
在常规的实现方式中Redis锁机制一般是由 setnx 命令实现是”set if not exists”的简写,语法setnx key value,将key设置值为value,如果key不存在会返回1,这种情况下等同 set 命令。 当key存在时,什么也不做会返回0,并且要使用 expire 设置一个锁的过期时间,避免应用程序异常导致锁一直没有释放。
例如:
127.0.0.1:6379> setnx key1 1
(integer) 1
127.0.0.1:6379> setnx key1 1
(integer) 0
127.0.0.1:6379> expire key1 60
(integer) 1
但是上面的setnx和expire实现分布式锁的方式是不安全,两条命令非原子性的,并不能保证一致性,可以通过一些第三方框架或者自己通过lua脚本实现原子操作,下面会通过代码分析分布式锁来实现。
四、分布式锁代码实现(解决分布式锁原子性、死锁、误删、可重入、自动续期等问题)
这里使用的是SpringBoot环境,会使用RedisTemplate的API操作Redis实现分布式锁,解决分布式锁原子性、死锁、误删、可重入、自动续期等问题。
需要SpringBoot集成调用Redis资料的可以跳转:https://blog.csdn.net/weixin_44606481/article/details/133907103
4.1、分布式锁实现工具类
这个分布式锁实现工具类已经将分布式锁原子性、死锁、误删、可重入、自动续期等问题都已解决,为了做演示和重点讲解问题解决步骤这里没有进行特定封装,可以根据需要自行封装增强拓展性。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;/*** @author Kerwin*/
@Component
public class RedisLockUtils {@Autowiredprivate RedisTemplate<String, String> redisTemplate;// 活跃锁key+value集合,续期的时候会使用private volatile static CopyOnWriteArraySet activeLockKeySet = new CopyOnWriteArraySet();// 定时线程池 用于续期private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);/*** 加锁* @Param key* @Param value 锁的value一般使用线程ID,在解锁时需要使用* @Param expireTime 过期时间 单位秒*/public boolean lock(String key, String value, long expireTime) {// 为了实现锁的可重入这里要自己封装一个lua脚本,如果不考虑可重入可以直接使用redisTemplate.opsForValue().setIfAbsent(K key, V value, long timeout, TimeUnit unit)String lockLua = getLockLua();boolean result = executeLua(lockLua, key, value, String.valueOf(expireTime));// 当加锁成功且活跃锁key+value不在集合中则添加续期任务if (result && !activeLockKeySet.contains(key + value)) {// 将活跃锁key+value放入集合中activeLockKeySet.add(key + value);// 加锁成功添加续期任务resetExpire(key, value, expireTime);}return result;}/*** 获取加锁lua脚本*/private String getLockLua() {// 封装加锁lua脚本 PS: 这个lua脚本应该是要定义成全局的,我这里为了演示定义成局部组装方便介绍每一步流程// lua脚本参数介绍 KEYS[1]:传入的key ARGV[1]:传入的value ARGV[2]:传入的过期时间// 在使用redisTemplate执行lua脚本时会传入key数组和参数数组,List<K> keys, Object... args,在lua脚本中取key值和参数值时使用KEYS和ARGV,数组下标从1开始StringBuilder lockLua = new StringBuilder();// 通过SETNX命令设置锁,如果设置成功则添加一个过期时间并且返回1,否则判断是否为重入锁lockLua.append("if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then\n");lockLua.append(" redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))\n");lockLua.append(" return 1\n");lockLua.append("else\n");// 当锁已经存在时,判断传入的value是否相等,如果相等代表为重入锁返回1并且重置过期时间,否则返回0lockLua.append(" if redis.call('GET', KEYS[1]) == ARGV[1] then\n");lockLua.append(" redis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))\n");lockLua.append(" return 1\n");lockLua.append(" else\n");lockLua.append(" return 0\n");lockLua.append(" end\n");lockLua.append("end");return lockLua.toString();}/*** 解锁* @Param key* @Param value 锁的value一般使用线程ID,在解锁时需要判断是当前线程才运行删除*/public boolean unlock(String key, String value) {// 为了实现避免误删锁,这里要自己封装一个lua脚本String unLockLua = getUnlockLua();boolean result = executeLua(unLockLua, key, value);if (result) {// 将活跃锁key+value从集合中删除activeLockKeySet.remove(key + value);}return result;}/*** 获取解锁lua脚本*/private String getUnlockLua() {// 封装解锁lua脚本 PS: 这个lua脚本应该是要定义成全局的,我这里为了演示定义成局部组装方便介绍每一步流程// lua脚本参数介绍 KEYS[1]:传入的key ARGV[1]:传入的value// 在使用redisTemplate执行lua脚本时会传入key数组和参数数组,List<K> keys, Object... args,在lua脚本中取key值和参数值时使用KEYS和ARGV,数组下标从1开始StringBuilder unlockLua = new StringBuilder();// 判断传入的锁key是否存在,如果不存在则直接返回1,如果存在则判断传入的value值是否和获取到的value相等unlockLua.append("if redis.call('EXISTS',KEYS[1]) == 0 then\n");unlockLua.append(" return 1\n");unlockLua.append("else\n");// 判断传入的value值是否和获取到的value相等,如果相等则代表是当前线程删除锁,执行删除对应key逻辑,然后返回1,否则返回0unlockLua.append(" if redis.call('GET',KEYS[1]) == ARGV[1] then\n");unlockLua.append(" return redis.call('DEL',KEYS[1])\n");unlockLua.append(" else\n");unlockLua.append(" return 0\n");unlockLua.append(" end\n");unlockLua.append("end");return unlockLua.toString();}/*** 封装redisTemplate执行lua脚本返回boolean类型执行器* @param scriptText lua脚本* @param key 传入数组keys的第一个元素这里就是我们锁key* @param args 传入数组args的第一个元素这里就是我们传入的value*/private boolean executeLua(String scriptText, String key, Object... args) {// 通过 DefaultRedisScript 来执行 lua脚本DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();// Boolean 对应 lua脚本返回的 0 1redisScript.setResultType(Boolean.class);// 指定需要执行的 lua脚本redisScript.setScriptText(scriptText);// 注意 需要提供 List<K> keys, Object... args 代表 keys 和 ARGVreturn redisTemplate.execute(redisScript, Collections.singletonList(key), args);}/*** 锁续期* @Param key* @Param value 锁的value一般使用线程ID,在解锁时需要使用* @Param expireTime 过期时间 单位秒,*/private void resetExpire(String key, String value, long expireTime) {// 如果key+value在集合中不存在,则不再进行续期操作if (!activeLockKeySet.contains(key + value)) {return;}//设置过期时间,推荐设置成过期时间的1/3时间续期一次,比如30s过期,10s续期一次long delay = expireTime <= 3 ? 1 : expireTime / 3;executorService.schedule(() -> {System.out.println("自动续期 key="+key+ " value="+value);// 执行续期操作,如果续期成功则再次添加续期任务,如果不成功则将不在进行任务续期,并且将活跃锁key+value从集合中删除if (executeLua(getResetExpireLua(), key, value, String.valueOf(expireTime))) {System.out.println("自动续期成功开启下一轮自动续期");resetExpire(key, value, expireTime);} else {System.out.println("自动续期失败锁key已经删除或不是指定value持有的锁,取消自动续期");activeLockKeySet.remove(key + value);}}, delay, TimeUnit.SECONDS);}/*** 获取锁续期lua脚本*/private String getResetExpireLua() {// 封装续期lua脚本 PS: 这个lua脚本应该是要定义成全局的,我这里为了演示定义成局部组装方便介绍每一步流程// lua脚本参数介绍 KEYS[1]:传入的key ARGV[1]:传入的value ARGV[2]:传入的过期时间// 在使用redisTemplate执行lua脚本时会传入key数组和参数数组,List<K> keys, Object... args,在lua脚本中取key值和参数值时使用KEYS和ARGV,数组下标从1开始StringBuilder resetExpireLua = new StringBuilder();// 判断传入的锁key是否存在且获取到的value值是否和传入的value值相等,如果相等则重置过期时间,然后返回1,否则返回0resetExpireLua.append("if redis.call('EXISTS',KEYS[1]) == 1 and redis.call('GET',KEYS[1]) == ARGV[1] then\n");resetExpireLua.append(" redis.call('EXPIRE',KEYS[1],tonumber(ARGV[2]))\n");resetExpireLua.append(" return 1\n");resetExpireLua.append("else\n");resetExpireLua.append(" return 0\n");resetExpireLua.append("end");return resetExpireLua.toString();}
}
4.2、测试分布式锁效果
这个测试类中模拟1000个用户抢10个商品,测试是否会出现超卖情况,提供了两个方法,一个使用分布式锁,一个不使用分布式锁,当使用分布式锁是不会出现超卖,当没有使用分布式时肯定会出现超卖。
import com.redisscene.utils.RedisLockUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;@RunWith(SpringRunner.class)
@SpringBootTest(classes = RedisSceneApplication.class)
public class RedisLockTest {@Autowiredprivate RedisLockUtils redisLockUtils;// 产品库存lock前缀private final String productLockKeyPrefix = "PRODUCT_LOCK_KEY:";// 模拟产品库存信息private static Map<String, String> productMap = new HashMap<>();static {productMap.put("id", "P0001");productMap.put("title", "分布式锁");productMap.put("stock", "10");productMap.put("sold", "0");}@Testpublic void t1() throws InterruptedException {// 定义一个线程池,队列根据需要设置ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 100, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));CountDownLatch countDownLatch = new CountDownLatch(1000);// 模拟10000个人抢10个商品for (int i = 0; i < 1000; i++) {executor.execute(() -> {// 加锁扣减库存deductStockLock();// 不加锁扣减库存
// deductStockNotLock();countDownLatch.countDown();});}countDownLatch.await();executor.shutdown();System.out.println("productMap=" + productMap.toString());}/*** 扣减库存:使用分布式锁*/private void deductStockLock() {// 通过UUID和线程ID组合成value,标识当前线程,解锁的时候判断是否是当前线程持有的锁String uuidValue = UUID.randomUUID() + ":" + Thread.currentThread().getId();// 组装锁keyString lockKey = productLockKeyPrefix + productMap.get("id");boolean lock = false;try {// 获取锁lock = redisLockUtils.lock(lockKey, uuidValue, 30);// 测试锁续期效果 这里模拟业务处理时间40s超过Thread.sleep(40000);// 再次获取锁,测试可重入效果lock = redisLockUtils.lock(lockKey, uuidValue, 30);// 如果没有获取到锁则直接返回if (!lock) {// 这里直接响应失败,也可以进行重试return;}System.out.println("获取锁成功 uuidValue=" + uuidValue);// 获取到锁执行业务逻辑,处理库存信息,假设每个线程每次购买1个商品Integer stock = Integer.valueOf(productMap.get("stock"));if (stock <= 0) {
// System.out.println("库存不足");return;}// 库存 - 1productMap.put("stock", String.valueOf(Integer.valueOf(productMap.get("stock")) - 1));// 已售 + 1productMap.put("sold", String.valueOf(Integer.valueOf(productMap.get("sold")) + 1));} catch (Exception e) {e.printStackTrace();} finally {if (lock) {// 解锁redisLockUtils.unlock(lockKey, uuidValue);}}}/*** 扣减库存:不使用分布式锁*/private void deductStockNotLock() {// 判断库存是否足够,假设每个线程每次购买1个商品Integer stock = Integer.valueOf(productMap.get("stock"));if (stock <= 0) {
// System.out.println("库存不足");return;}// 暂停10毫秒方便呈现不加锁超卖效果try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}// 库存 - 1productMap.put("stock", String.valueOf(Integer.valueOf(productMap.get("stock")) - 1));// 已售 + 1productMap.put("sold", String.valueOf(Integer.valueOf(productMap.get("sold")) + 1));}
}
五、分布式锁常见问题以及解决方法
在章节四中的分布式锁实现工具类中已经将下面的问题都解决,并且注释的也比较详细。
5.1、分布式锁死锁问题
5.1.1、逻辑说明
使用分布式锁最害怕的问题就是出现死锁,自己业务上写出的死锁这里不做说明,这里只介绍异常情况出现死锁应该如何解决,在业务异常没有处理好或者应用服务宕机没有解锁就会出现死锁问题,锁key一直存储在Redis中不会被释放,后续业务恢复去获取锁时因为锁已经存在一直都无法获取到锁,这就是死锁问题,但是死锁问题很好解决,只要给锁key加上一个过期时间即可。
5.1.2、解决方案
- 代码中使用RedisTemplate添加过期时间
// setIfAbsent方法等同于setnx,当这个key不存在时插入成功返回true,当key存在时不做处理返回false
boolean lock = redisTemplate.opsForValue().setIfAbsent(key1, value1);
// 加锁成功设置一个30s过期时间
if(lock){redisTemplate.expire(key,30,TimeUnit.SECONDS);
}
- 存在问题
获取锁和添加过期时间是两步操作并没有原子性,在并发操作时会存在问题,下面会通过lua脚本来解决原子性问题。
5.2、分布式锁原子性问题
5.2.1、逻辑说明
在5.1中给锁key设置超时时间解决了死锁问题,但是因为是分为两个步骤操作,需要分别调用Redis不具备原子性,要保证原子性只要保证将两个步骤合并成一个Redis调用即可,核心思想是通过lua脚本来实现,可以直接通过RedisTemplate操作Redis执行lua脚本,Redis执行lua脚本也是单线程的所以可以保证原子性。
5.2.2、解决方案
- 1、使用
lua脚本实现加锁并且添加过期时间
-- KEYS[1]:传入的key ARGV[1]:传入的value ARGV[2]:传入的过期时间
-- 使用setnx插入key,如果成功给key设置一个过期时间然后返回1,如果失败直接返回0
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 thenredis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))return 1
elsereturn 0
end
- 2、使用
RedisTemplate的setIfAbsent方法,setIfAbsent方法等同于setnx,而且这个方法还实现了原子性给key添加过期时间操作,具体实现和我们上面lua脚本类似
// 当这个key不存在时插入成功并且设置一个超时时间然后返回true,当key存在时不做处理返回false
boolean lock = redisTemplate.opsForValue().setIfAbsent(key1, value1, 30, TimeUnit.SECONDS);
5.3、分布式锁可重入问题
5.3.1、逻辑说明
可重入锁又名递归锁:是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了,就很麻烦。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁,分布式锁实现可重入也比较简单只要在保证原子性的lua脚本中在判断value值即可。
5.3.2、解决方案
lua脚本实现
-- KEYS[1]:传入的key ARGV[1]:传入的value ARGV[2]:传入的过期时间
-- 通过SETNX命令设置锁,如果设置成功则添加一个过期时间并且返回1,否则判断是否为重入锁
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 thenredis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))return 1
else-- 当锁已经存在时,判断传入的value是否相等,如果相等代表为重入锁返回1并且重置过期时间,否则返回0if redis.call('GET', KEYS[1]) == ARGV[1] thenredis.call('EXPIRE', KEYS[1], tonumber(ARGV[2]))return 1elsereturn 0end
end
5.4、分布式锁如何防止误删
5.4.1、逻辑说明
持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删,要想解决误删只需要判断一下这把锁是否属于自己,只能删除属于自己的锁,这里会将一个UUID + 线程ID 作为锁的 value,在删除锁的时候判断value值是否相同即可。
5.4.2、解决方案
lua脚本实现
-- KEYS[1]:传入的key ARGV[1]:传入的value
-- 判断传入的锁key是否存在,如果不存在则直接返回1,如果存在则判断传入的value值是否和获取到的value相等
if redis.call('EXISTS',KEYS[1]) == 0 thenreturn 1
else-- 判断传入的value值是否和获取到的value相等,如果相等则代表是当前线程删除锁,执行删除对应key逻辑,然后返回1,否则返回0if redis.call('GET',KEYS[1]) == ARGV[1] thenreturn redis.call('DEL',KEYS[1])elsereturn 0end
end
5.5、分布式锁自动续期问题
5.5.1、逻辑说明
假设我们给某个业务分布式锁设置了30s的过期时间,但是这个业务要执行40s,在30s后锁过期其它线程也可以获取到这把锁,但是前一个线程还没有执行完毕,这样显然是有问题的,要对还没有执行完的业务进行锁的自动续期操作。
5.5.2、解决方案
我这里采用的是定时线程池ScheduledExecutorService来实现,在加锁时同步给定时线程池添加一个定时任务,定时时间一般设置为过期时间的1/3,其中还有考虑重入问题会使用一个set集合存储key+value的组合值,每个key+value只能被添加一次,续期方法在 4.1、分布式锁实现工具类的resetExpire方法中有详细描述。
相关文章:
使用 Redis 实现分布式锁,解决分布式锁原子性、死锁、误删、可重入、自动续期等问题(使用SpringBoot环境实现)
目录 一、前言二、分布式锁具备的特点三、Redis分布式锁的实现核心思路四、分布式锁代码实现(解决分布式锁原子性、死锁、误删、可重入、自动续期等问题)4.1、分布式锁实现工具类4.2、测试分布式锁效果 五、分布式锁常见问题以及解决方法5.1、分布式锁死…...
mysql oracle统计报表每天每月每年SQL
mysql查询当天、昨天、本周、上周、近7天、近30天、本月、上个月、近6个月、本季度、上季度、本年和去年的数据 注意 在 XML 中 < 应该转为 < 当天 SELECT * FROM 表名 WHERE TO_DAYS(时间字段名) TO_DAYS(NOW()); 昨天 SELECT * FROM 表名 WHERE TO_DAYS(NOW()) - TO…...
通过Python设置及读取PDF属性,轻松管理PDF文档
PDF文档属性是嵌入在PDF文档中的一些与文档有关的信息,如作者、制作软件、标题、主题等。PDF属性分为默认属性和自定义属性两种,其中默认属性是一些固定的文档信息,部分信息自动生成(如文件大小、页数、页面大小等信息)…...
10. 深度学习——模型优化
机器学习面试题汇总与解析——模型优化 本章讲解知识点 前言低秩近似剪枝与稀疏约束参数量化二值网络知识蒸馏紧凑的网络结构本专栏适合于Python已经入门的学生或人士,有一定的编程基础。本专栏适合于算法工程师、机器学习、图像处理求职的学生或人士。本专栏针对面试题答案进…...
macos 上彻底卸载 DevEco Studio
1. 退出DevEco Studio: 确保DevEco Studio没有在运行。如果它在Dock中,可以右键点击其图标,然后选择退出。或者使用Command Q快捷键确保应用程序完全退出。 2. 删除DevEco Studio应用程序: 打开“应用程序”文件夹&#x…...
Nginx(五) break,if,return,rewrite和set指令的执行顺序深究
本篇文章主要对break,if,return,rewrite和set这5个指令的执行顺序进行深究,如需了解这5个指令的功能和配置,请参考另一篇文章 Nginx(三) 配置文件详解 由于文章篇幅较长,所以我就先把结论贴出来,…...
八大学习方法(金字塔模型、费曼学习法、布鲁姆学习模型)
在微博上看到博主发的,觉得总结很好,在此摘录:...
K8S的基础知识
K8S的意义与入门 专有名词 容器:包含了运行一个应用程序所需要的所有东西,包括:代码、运行时、各种依赖和配置。pod:K8s调度的最小单元,包含一个或多个容器。一个容器组中的容器具有紧密耦合性,共享资源,存储空间和IP。即同一个容器组中的容器可以通过localhost:xxx访问…...
java:基于jjwt写一个jwt工具类
背景 在Java中,使用JWT(JSON Web Tokens)相关的包通常包括以下内容: jjwt:JJWT是一个非常流行的Java JWT库,它提供了简单易用的API来创建和验证JWT。jose4j:JOSE4J是一个用于处理JSON Web签名…...
AK F.*ing leetcode 流浪计划之半平面求交
欢迎关注更多精彩 关注我,学习常用算法与数据结构,一题多解,降维打击。 本期话题:半平面求交 背景知识 学习资料 视频讲解 https://www.bilibili.com/video/BV1jL411C7Ct/?spm_id_from333.1007.top_right_bar_window_history…...
docker搭建zokeeper集群、kafka集群
三台机器,ip分别为ip1,ip2,ip3 一、安装docker集群 1、三台机器分别拉取镜像 docker pull wurstmeister/zookeeper 2、三台机器分别运行容器 (1)第一台 docker run -d --restartalways --log-driver json-file --log-opt max-size100m --lo…...
【java学习—十四】反射机制调用指定方法、指定属性(5)
文章目录 1. 调用指定方法2. 调用指定属性 1. 调用指定方法 通过反射,调用类中的方法,通过 Method 类完成。步骤: ①通过 Class 类的 getMethod(String name,Class...parameterTypes) 方法取得一个 Method 对象,并设置此…...
PC端微信@所有人逻辑漏洞
(一)过程 这个漏洞是PC端微信,可以越权让非管理员艾特所有人,具体步骤如下 第一步:找一个自己的群(要有艾特所有人的权限)“123”是我随便输入的内容,可以更改,然后按c…...
如何在Windows 10中进行屏幕截图
本文介绍如何在Windows 10中捕获屏幕截图,包括使用键盘组合、使用Snipping Tool、Snipp&Sketch Tool或Windows游戏栏。 使用打印屏幕在Windows 10中捕获屏幕截图 在Windows 10中捕获屏幕截图的最简单方法是按下键盘上的PrtScWindows键盘组合。你将看到屏幕短暂…...
【nlp】2.4 GRU模型
GRU模型 1 GRU介绍2 GRU的内部结构图2.1 GRU结构分析2.2 Bi-GRU介绍2.3 使用Pytorch构建GRU模型2.4 GRU优缺点3 RNN及其变体1 GRU介绍 GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆…...
国科云:浅谈DNS缓存投毒常见类型和防御策略
为了提升解析效率减轻各级服务器的解析压力,DNS系统中引入了缓存机制,但这同样也带来了较大的安全隐患,为攻击者利用DNS缓存进行投毒攻击创造了条件,对DNS系统的安全造成了巨大破坏。本文国科云将分析缓存投毒的两种主要类型&…...
Linux命令(120)之tcpdump
linux命令之tcpdump 1.tcpdump介绍 linux命令tcpdump是用来将网络中传送的数据包完全截获下来以进行相关分析,常用的分析工具是wireshark 2.tcpdump用法 tcpdump [参数] tcpdump参数 参数说明-i指定端口-n指定协议-t在输出的每一行不打印时间戳-s抓取数据包时&a…...
2311rust对接C
原文 为了与其他语言通信,Rust提供了(FFI)外部函数接口.FFI是Rust和C间的函数调用,与C函数调用有相同性能的零成本抽象. FFI绑定还可利用(如所有权和借用)语言功能来提供强制指针和其他资源协议的安全接口. Rust与C对话 从Rust调用C代码的简单示例开始.如下为C代码: int do…...
MYSQL字符串函数详解和实战(字符串函数大全,内含示例)
MySQL提供了许多字符串函数,用于处理和操作字符串数据。以下是一些常用的MYSQL字符串函数。 建议收藏以备后续用到查阅参考。 目录 一、CONCAT 拼接字符串 二、CONCAT_WS 拼接字符串 三、SUBSTR 取子字符串 四、SUBSTRING 取子字符串 五、SUBSTRING_INDEX 取子…...
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)
目录 1. C多线程 1.1 thread库 1.2 mutex库 1.3 RAII锁 1.4 atomicCAS 1.5 condition_variable 1.6 分别打印奇数和偶数 2. shared_ptr线程安全 2.1 库里面的shared_ptr使用 2.2 shared_ptr加锁代码 3. 单例模式线程安全 3.1 懒汉模式线程安全问题 3.2 懒汉模式最…...
【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
相机Camera日志实例分析之二:相机Camx【专业模式开启直方图拍照】单帧流程日志详解
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了: 这一篇我们开始讲: 目录 一、场景操作步骤 二、日志基础关键字分级如下 三、场景日志如下: 一、场景操作步骤 操作步…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
