Redisson实现分布式锁

Redisson实现分布式锁

一、分布式锁使用场景

随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景,而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,在这种情况下,我们就需要用到分布式锁

首先我们先来看一个小例子:

假设某商城有一个商品库存剩10个,用户A想要买6个,用户B想要买5个,在理想状态下,用户A先买走了6了,库存减少6个还剩4个,此时用户B应该无法购买5个,给出数量不足的提示;而在真实情况下,用户A和B同时获取到商品剩10个,A买走6个,在A更新库存之前,B又买走了5个,此时B更新库存,商品还剩5个,这就是典型的电商“秒杀”活动。

  • 案例:
@RestController
public class HelloController {//数据库中的库存private Integer num=10;@GetMapping("/")public Integer secondsKill(int count){synchronized (num){//是否有库存if(num>0){//减库存num=num-count;}}return num;}
}

思考:将该项目启动两个实例,8080实例购买6个,8081实例购买5个此时会有什么问题

从上述例子不难看出,在高并发情况下,如果不做处理将会出现各种不可预知的后果。那么在这种高并发多线程的情况下,解决问题最有效最普遍的方法就是给共享资源或对共享资源的操作加一把锁,来保证对资源的访问互斥。在Java JDK已经为我们提供了这样的锁,利用ReentrantLock或者synchronized,即可达到资源互斥访问的目的。但是在分布式系统中,由于分布式系统的分布性,即多线程和多进程并且分布在不同机器中,也就是说一个服务可以同时启动多个实例,不同用户访问不同的实例,那么这两种锁将失去原有锁的效果,需要我们自己实现分布式锁——分布式锁。

一般我们使用分布式锁有两个场景:

  • 效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源。比如用户付了钱之后有可能不同节点会发出多封短信。
  • 正确性:加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。

Redis 因为其性能好,实现起来分布式锁简单,所以让很多人都对基于 Redis 实现的分布式锁十分青睐。

提示

除了能使用 Redis 实现分布式锁之外,Zookeeper 也能实现分布式锁。但是项目中不可能仅仅为了实现分布式锁而专门引入 Zookeeper ,所以,除非你的项目体系中本来就有 Zookeeper(来实现其它功能),否则不会单独因为分布式锁而引入它

二、使用Redis的Setnx命令实现分布式锁

  • 步骤分析

    1、每次用户请求下单时,就在redis中设置一个键值对,如果设置成功,就执行下单操作流程
    2、如果下单失败,就让你等待,等到前面的人下单完成后将该键删除,你再下单
    

1、SETNX 命令

早期,SETNX 是独立于 SET 命令之外的另一条命令。它的意思是 SET if Not eXists,即,在键值对不存在的时候才能设值成功。

注意:SETNX 命令的价值在于:它将 判断设值 两个操作合二为一,从而避免了 查查改改 的情况的出现。

后来,在 Redis 2013 年推出的 2.6.12 版本中,Redis 为 SET 命令官方提供了 NX 选项,使得 SET 命令也能实现 SETNX 命令的功能。其语法如下:

SET <key> <value> [EX seconds] [PX milliseconds] [NX | XX]

EX 值的是 key 的存活时间,单位为秒。PXEX 作用一样,唯一的不同就是后者的单位是微秒(使用较少)。

NXXX 作用是相反的。NX 表示只有当 key『不存在时』才会设置其值;XX 表示当 key 存在时才设置 key 的值。

在 “升级” 了 SET 命令之后,Redis 官方说:“由于 SET 命令选项可以替换 SETNX,SETEX,因此在 Redis 的将来版本中,这二个命令可能会被弃用并最终删除”。

所以,现在我们口头所说的 SETNX 命令,并非单指 SETNX 命令,而是包括带 NX 选项的 SET 命令(甚至以后就没有 SETNX 命令了)

2、SETNX 的使用

在使用 SETNX 操作实现分布式锁功能时,需要注意以下几点:

  • 这里的『锁』指的是 Redis 中的一个约定的键值对。谁能创建这个键值对,就意味着谁拥有这整个『锁』。
  • 使用 SETNX 命令获取『锁』时,如果操作返回结果是 0(表示 key 已存在,设值失败),则意味着获取『锁』失败(该锁被其它线程先获取),反之,则设值成功,表示获取『锁』成功。
    • 如果这个 key 不存在,SETNX 才会设置该 key 的值。此时 Redis 返回 1 。
    • 如果这个 key 存在,SETNX 则不会设置该 key 的值。此时 Redis 返回 0 。
  • 为了防止其它线程获得『锁』之后,有意或无意,长期持有『锁』而不释放(导致其它线程无法获得该『锁』)。因此,需要为 key 设置一个合理的过期时间。
  • 当成功获得『锁』并成功完成响应操作之后,需要释放『锁』(可以执行 DEL 命令将『锁』删除)。

在代码层面,与 Setnx 命令对应的接口是 ValueOperations 的 setIfAbsent 方法

  • 示例代码一:
@RestController
public class HelloController {@Autowiredprivate StringRedisTemplate redisTemplate;@SneakyThrows@GetMapping("/add")public String addGoods(String goodsname) {String message="";ValueOperations<String, String> operations = redisTemplate.opsForValue();//在redis中设置一个键,表示有人正在操作库存boolean flag = operations.setIfAbsent(goodsname, "y",30, TimeUnit.SECONDS);if (flag) {Thread.sleep(10000);try {message= "修改了商品信息";} catch (Exception e) {//删除redis中的键redisTemplate.delete(goodsname);}}else{message= "其他人正在使用,请稍后重试";}return message;}
}
  • 示例代码二:
	@SneakyThrows@GetMapping("/add2")public String addGoods2(String goodsname) {String message = "";ValueOperations<String, String> operations = redisTemplate.opsForValue();//在redis中设置一个键,表示有人正在操作库存,返回false表示正在有人操作此时会失败//如果失败了隔两秒重试一次while (!operations.setIfAbsent(goodsname, "y", 30, TimeUnit.SECONDS)) {//睡100 毫秒,继续取set  看看是否成功System.out.println(Thread.currentThread().getName() + ":获取锁失败");Thread.sleep(1000);}try {message= "修改了商品信息"} catch (Exception e) {//删除redis中的键redisTemplate.delete(goodsname);}return message;}

开启两个不同的浏览器发请求测试

3、SETNX命令的问题

a、死锁问题

假设线程1通过SETNX获取到锁并且正常执行然后释放锁那么一切ok,其它线程也能获取到锁。但是线程1现在"耍脾气"了,线程1抱怨说"工作太久有点累需要休息一下,你们想要获取锁等着吧,等我把活干完你们再来获取锁"。此时其它线程就无法向下继续执行,因为锁在线程1手中。这种长期不释放锁情况就有可能造成死锁。
为了防止像线程1这种"耍脾气"的现象发生,我们可以设置key的过期时间来解决。设置过期时间过后其它线程可不会惯着线程1,其它线程表示你要休息可以,休息了指定时间把锁让出来然后拍拍屁股走人,没人惯着你。

上锁时,设置的超时自动删除时长(3 秒),设置多长合适?万一设置短了怎么办?如果设置短了,在业务逻辑执行完之前时间到期,那么 Redis 自动就把键值对给删除了,即,把锁给释放了,这不符合逻辑。
b、SETNX误删情况
  • 情况一

    设置过期时间线程1被治得服服帖帖,此时线程1又开始不当人了。线程1想既然你抢我得锁,等你获得锁后我就将锁删除毕竟我还要有备用钥匙,让你也锁不住,让其它线程也执行。
    线程1休息的时间超过了过期时间,此时锁会自动释放。线程2现在脱颖而出抢到了锁然后开心的继续执行。但是现在线程1醒了,发现线程2抢走了锁。线程1表示小子胆挺肥啊,敢抢我的锁,等我执行完了就将你锁删除,让其它"哥们"也进来。此时就会发生蝴蝶效应,线程1删除了线程2的锁,线程2删除了线程3的锁,直到最后一个"哥们:wc,我锁了?"。当然线程是无感知,其实线程1乃至其它线程都不知道删除的是别人的锁,全部线程都以为删除的是自己的锁。直到最后一个线程无锁可删。
    这种误删锁的情况让锁的存在荡然无存,本来应该串行执行的线程,在一定程度上都开始并发执行了。
    那么误删情况该如何解决了?

    我们可以给锁加上线程标识,只有锁是当前线程的才能删除,否则不能删除。在添加key的时候,key的value存储当前线程的标识,这个标识只要保证唯一即可。可以使用UUID或者一个自增数据。在删除锁的时候,将线程标识取出来进行判断,如果相同就表示锁是自己的能够删除,否则不能删除。

    获取锁

    //获取线程前缀,同时也是线程表示。通过UUID唯一性
    private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    //与线程id组合
    public boolean tryLock(long timeOut) {//获取线程idString id =ID_PREFIX+ Thread.currentThread().getId();//获取锁Boolean absent = redisTemplate.opsForValue().setIfAbsent(key, id , timeOut, TimeUnit.SECONDS);return Boolean.TRUE.equals(absent);}
    

    释放锁:

    public void unLock() {//获取存储的线程标识String value = stringRedisTemplate.opsForValue().get(key);//当前线程的线程标识String id =ID_PREFIX+ Thread.currentThread().getId();//线程标识相同则删除否,则不删除if (id.equals(value)){redisTemplate.delete(key);}}
    
  • 情况二

    加入线程标识后,线程一不能随便删除其它线程的锁,但是线程1又开始不当人了。线程1表示判断线程标识和释放锁的操作我可以分开执行,这又不是一个原子性的操作,线程1干完活以后就准备去释放锁,当线程1判断锁是自己的后表示开锁太累了,休息一会在开。此时其它线程就想无所谓,反正过期时间一到锁就会自动释放。但是线程1已经判断了锁是自己的以后就不会执行判断锁的操作(线程1已经执行了if判断,只是没有执行方法体),当线程2获得锁后,线程1仍然能删除线程2的锁。

    解锁时,`查 - 删` 操作是 2 个操作,由两个命令完成,非原子性。
    redis底层执行这个setnx不是一个原子操作,而是有两步操作完成的,首先set hello world,然后第二步设置key的过期时间: expire hello 3,那么如果执行完第一步刚好redis宕机了,此时key一直保存到redis。永远也无法删除了。
    

三、Redisson实现分布式锁【日常使用】

1、Redisson 如何解决上述问题

  • Redisson 解决 “过期自动删除时长” 问题的思路和方案

    Redisson 中客户端一旦加锁成功,就会启动一个后台线程(惯例称之为 watch dog 看门狗)。watch dog 线程默认会每隔 10 秒检查一下,如果锁 key 还存在,那么它会不断的延长锁 key 的生存时间,直到你的代码中去删除锁 key 。

  • Redisson 解决setnx和 解锁的非原子性 问题的思路和方案

    Redisson 的上锁和解锁操作都是通过 Lua 脚本实现的。Redis 中 执行 Lua 脚本能保证原子性,整段 Lua 脚本的执行是原子性的,在其执行期间 Redis 不会再去执行其它命令

2、Redisson 的使用

  • 添加依赖

    <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.15.6</version>
    </dependency>
    
  • 配置 RedissonConfig

    /*** redission配置类*/
    @Configuration
    public class RedissonConfig {@Value("${spring.redis.host}")private String host;@Value("${spring.redis.port}")private String port;@Value("${spring.redis.database}")private int database;@Beanpublic RedissonClient getRedissonClient(){String address="redis://"+host+":"+port; //拼接redis地址Config config = new Config();config.useSingleServer().setAddress(address).setDatabase(database).setKeepAlive(true);return Redisson.create(config);}
    }
    
a、基本用法
@RestController
public class HelloController {@Autowiredprivate RedissonClient redissonClient;@SneakyThrows@GetMapping("/add")public String addGoods(String goodsname) {String message = "";//获取一把锁对象,一般将需要上锁的类名+方法名做为锁的键RLock rLock=redissonClient.getLock("HelloController.addGoods");try{rLock.lock();  //加锁,其实就是设置一个key-value   默认加的锁都是30s}catch (Exception e){e.printStackTrace();return "上锁失败,请稍后重试";}try{Thread.sleep(5000);//睡五秒message = "执行了添加操作";}catch (Exception e){e.printStackTrace();return "上锁失败,请稍后重试";}finally {//判断是否有锁对象,以及是否是同一个锁if (rLock.isLocked() && rLock.isHeldByCurrentThread()){rLock.unlock();  //解锁}}return message;}
}
b、批量加锁

RedissonMultiLock 对象可以将多个 RLock 对象关联为一个联锁,每个 RLock 对象实例可以来自于不同的 Redisson 实例

@RestController
public class HelloController {@Autowiredprivate RedissonClient redissonClient;@SneakyThrows@GetMapping("/add")public String addGoods(String goodsname) {String message = "";//批量锁对象RLock multiLock=null;try{//保存批量锁对象List<RLock> rLockList=new ArrayList<RLock>();for(String skid :skuList){RLock rLock=redissonClient.getLock("lock:"+skid);rLockList.add(rLock);}//将锁集合转为数组RLock[] arrayLock = rLockList.stream().toArray(RLock[]::new);//将多个RLock整合为一个大锁对象multiLock=redissonClient.getMultiLock(arrayLock);multiLock.lock(); //上锁}catch (Exception e){e.printStackTrace();return "上锁失败,请稍后重试";}try{message="执行了批量操作"}catch (Exception e){return "批量操作失败"}finally{//判断是否有锁对象,以及是否是同一个锁if (multiLock.isLocked() && multiLock.isHeldByCurrentThread()){multiLock.unlock();  //解锁}}return message;}
}

3、Redisson分析

  • 你通过 RedissonClient 拿到的锁都是 “可重入锁

    这里的 “可重入” 的意思是:持有锁的线程可以反复上锁,而不会失败,或阻塞等待;锁的非持有者上锁时,则会失败,或需要等待。当然,如果你对一个锁反复上锁,那么逻辑上,你应该对它执行同样多次的解锁操作

    @Autowired
    private RedissonClient redissonClient;
    @Test
    void contextLoads() {RLock rLock = redissonClient.getLock("hello");rLock.lock(); System.out.println("lock success!");rLock.lock(); System.out.println("lock success!");rLock.lock(); System.out.println("lock success!");rLock.unlock();rLock.unlock();rLock.unlock();
    }
    

    使用 lock( )上锁时由于你没有指定过期删除时间,所以,逻辑上只有当你调用 unlock( )之后,Redis 中代表这个锁的键值对才会被删除。当然你也可以在 lock 时指定超时自动解锁时间:

    rLock.lock(3,TimeUnit.SECONDS);  //3秒钟 自动解锁
    

    这种情况下,如果你有意或无意没有调用 unlock 进行解锁,那么 3秒后,Redis 也会自动删除代表这个锁的键值对

  • 当两个不同的线程对同一个锁进行 lock 时,第二个线程的上锁操作会失败

    而上锁失败的默认行为是阻塞等待,直到前一个线程释放掉锁。这种情况下,如果你不愿意等待,那么你可以调用 tryLock() 方法上锁。tryLock 上锁会立刻(或最多等一段时间)返回,而不会一直等(直到所得持有线程释放)。

    // 拿不到就立刻返回
    rLock.tryLock();
    // 拿不到最多等 1 秒。1 秒内始终拿不到,就返回
    rLock.tryLock(1, TimeUnit.SECONDS);
    // 尝试在1s内去拿锁,拿不到就返回false,拿到了10s自动释放这个锁
    rLock.tryLock(1, 10, TimeUnit.SECONDS);
    
  • Redisson 在上锁时,向 Redis 中添加的键值对时,通过hset设置k-v的

    其中键就是hello,当然你也可以是其它的值,那么这个值里面的键是redisson内部帮我们生成的UUID +“:” +thread-id 拼接而成的字符串;值是这个锁的上锁次数,默认是1

    Redisson 如何保证线程间的互斥以及锁的重入(反复上锁)?

    因为代表着锁的键值对的键中含有线程 ID ,因此,当你执行上锁操作时,Redisson 会判断你是否是锁的持有者,即,当前线程的 ID 是否和键值对中的线程 ID 一样。

    如果当前执行 lock 的线程 ID 和之前执行 lock 成功的线程的 ID 不一致,则意味着是 “第二个人在申请锁” ,那么就 lock 失败;如果 ID 是一样的,那么就是 “同一个” 在反复 lock,那么就累加锁的上锁次数,即实现了重入。

4、watch dog 自动延期机制

如果在使用 lock/tryLock 方法时,你指定了超时自动删除时间,如:hello.tryLock(10, TimeUnit.SECONDS);Redis 会自动10s后将当前线程锁的键值对给删除掉,不会自动续期,而且如果你的业务执行时间过长,超过了key的过期时间, 而你在执行完业务之后也去删除这个key,就会报错,提示错误为:当前线程不能删除这个key,因为你删的key不是你之前的key,而是另外一个线程给redis重新设置的key。所以设置带过期时间的hello.tryLock(10, TimeUnit.SECONDS)键值对时,时长一定要超过业务执行的时长

如果,你在使用 lock/tryLock 方法时,没有指定超时自动删除时间,那么,就完全依靠你的手动删除( unlock 方法 ),那么,这种情况下你会遇到一个问题:如果你有意或无意中忘记了 unlock 释放锁,那么锁背后的键值对将会在 Redis 中长期存在!

一定要注意Redisson 看门狗(watch dog)在指定了加锁时间时,是不会对锁时间自动续租的。

在 watch dog 机制中,有一个被 “隐瞒” 的细节:表面上看,你的 lock 方法没有指定锁定时长,但是 Redisson 去 Redis 中添加代表锁的键值对时,它还是添加了自动删除时间。默认 30 秒(可配置)。这意味着,如果,你没有主动 unlock 进行解锁,那么这个代表锁的键值对也会在 30 秒之后被 Redis 自动删除,但是实际上,并没有。这正是因为 Redisson 利用 watch dog 机制对它进行了续期( 使用 Redis 的 expire 命令重新指定新的过期时间)。也就是内部有一个定时任务,每隔10s会会自动启动定时任务,该任务重新给key续期30s。

5、使用aop统一实现分布式锁

  • 添加aop依赖

    <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
    </dependency>
    
  • 修改yml

    spring:aop:auto: true #开启aop
    
  • 创建自定义注解

    /*** 分布式锁自定义注解,表示哪些方法需要实现分布式锁*/
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface ReCommit {}
    
  • 创建apo类

    /*** 防止重复提交aop*/
    @Component
    @Aspect
    public class ReCommitAop {@Resourceprivate RedissonClient redissonClient;//配置节入点@Pointcut("execution(* com.woniu.controller.*.*(..))")public void pott(){ }/*** 环绕通知* @param point* @return*/@Around("pott()")public Object aroundPrintLog(ProceedingJoinPoint point){Object result=null;RLock rLock=null;try{//获得方法信息MethodSignature methodSignature = (MethodSignature) point.getSignature();//获得了要执行的方法的字节码对象Method method = methodSignature.getMethod();//获得方法中所有的形参Object[] args = point.getArgs();//判断方法上是否有注解ReCommit reCommit=method.getDeclaredAnnotation(ReCommit.class);if(reCommit!=null) { //存在,需要进行上锁String key=point.getSignature().toShortString();rLock= redissonClient.getLock(key);//加锁if (!rLock.tryLock()) {return new ResponseResult<Object>(6002,"数据使用中,请稍后再试。。。");}}//执行方法result = point.proceed(args);}catch (Throwable throwable) {throwable.printStackTrace();}finally{// 判断是否有锁对象,以及是否是同一个锁if (rLock!=null && rLock.isLocked() && rLock.isHeldByCurrentThread()){rLock.unlock();  //解锁}}return result;}
    }
    

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

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

相关文章

韩顺平 | 零基础快速学Python(16) 文件处理

文件 输入与输出 输入&#xff1a;数据从数据源(文件)到程序(内存)&#xff1b; 输出&#xff1a;数据从程序(内存)到数据源(文件)。 #mermaid-svg-06PG6JZq4jJMV1oH {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-sv…

数据库查询:查询入参类型和数据库字段类型不匹配导致的问题

问题&#xff1a;假设我们现在有这样的一张表 CREATE TABLE test_person (id int(20) NOT NULL COMMENT 主键,name varchar(20) DEFAULT NULL COMMENT 姓名,gender char(2) DEFAULT NULL COMMENT 性别,birthday date DEFAULT NULL COMMENT 生日,created_time timestamp NULL D…

JS-33-jQuery02-选择器

一、单个选择器 选择器是jQuery的核心。 一个选择器写出来类似$(#dom-id)。 美元符号定义 jQuery 为什么jQuery要发明选择器&#xff1f;回顾一下DOM操作中我们经常使用的代码&#xff1a; // 按ID查找&#xff1a; var a document.getElementById(dom-id);// 按tag查找&am…

开发了一个在线客服系统

开发了一个在线客服系统 作为一名热爱编程的开发者&#xff0c;我最近完成了一个令人兴奋的项目——一个在线客服系统。这个系统是我使用Golang、MySQL、Vue2和ElementUI技术栈精心打造的结果。 技术栈选择 Golang&#xff1a;我选择Golang作为后端开发语言&#xff0c;因为…

Java的maven项目导入本地jar包的三种方式

文章目录 Java的maven项目导入本地jar包的三种方式1、在项目中创建一个lib文件夹&#xff0c;将想要使用的本地jar包放进去2、方法一&#xff1a;直接在pom.xml中添加下列依赖&#xff08;项目协作推荐&#xff09;3、方法二&#xff1a;在项目结构中引用lib文件夹&#xff08;…

ATA-2048高压放大器在铁电材料中有什么应用

铁电材料是一类具有特殊电学性质的材料&#xff0c;它们能够在外加电场的作用下产生可逆的电极化&#xff0c;这种电极化可以在没有外加电场时保持。这使得铁电材料在许多应用中具有重要价值&#xff0c;特别是在电子设备和传感器领域。高压放大器作为一种电子设备&#xff0c;…

C++:Hash应用【位图与布隆过滤器】

什么是位图&#xff1f; 我们先来看一个问题&#xff1a; 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数是否在 这40亿个数中。【腾讯】 如果我们使用unordered_set容器来解决&#xff0c;40亿个数据&#xff0c;每个数据…

MaxCompute 近实时增全量处理一体化新架构和使用场景介绍

随着当前数据处理业务场景日趋复杂&#xff0c;对于大数据处理平台基础架构的能力要求也越来越高&#xff0c;既要求数据湖的大存储能力&#xff0c;也要求具备海量数据高效批处理能力&#xff0c;同时还可能对延时敏感的近实时链路有强需求&#xff0c;本文主要介基于 MaxComp…

python将pdf转为docx

如何使用python实现将pdf文件转为docx文件 1.首先要安装pdf2docx库 pip install pdf2docx2.实现转换 from pdf2docx import Converterdef convert_pdf_to_docx(input_pdf, output_docx):# 创建一个PDF转换器对象pdf_converter Converter(input_pdf)# 将PDF转换为docx文件pdf…

护眼台灯哪个牌子好?护眼灯十大品牌推荐,谁用谁真香

对于有子女的家庭&#xff0c;特别是那些热爱阅读的&#xff0c;晚上看书时更应该注重光线的问题&#xff0c;不然一旦光线过暗就容易导致视觉疲劳&#xff0c;进而演化为近视。因此&#xff0c;除了打开房间的灯&#xff0c;在桌面上放置一台护眼台灯更加能够保护眼睛。然而&a…

社交网络与Web3:数字社交的下一阶段

随着信息技术的飞速发展&#xff0c;人们的社交方式也发生了巨大的变化。从最初的互联网聊天室到如今的社交网络平台&#xff0c;我们已经见证了数字社交的不断演变和发展。而随着区块链技术的兴起&#xff0c;Web3时代的到来将为数字社交带来全新的可能性和挑战。本文将探讨社…

ADAPTIVE GRAPH DIFFUSION NETWORKS(自适应图扩散网络) 笔记

1 Title ADAPTIVE GRAPH DIFFUSION NETWORKS&#xff08;Chuxiong Sun, Jie Hu, Hongming Gu, Jinpeng Chen, Mingchuan Yang&#xff09;【2022】 2 Conclusion This study proposes the Adaptive Graph Diffusion Networks (AGDNs) which perform multi-layer generalized g…

MongoDB【五】索引

在MongoDB中&#xff0c;索引对于提升查询性能至关重要&#xff0c;特别是针对大型数据集时。以下是如何在MongoDB中创建、查看和删除索引的基本操作&#xff0c;以及不同类型的索引如何影响查询性能。 创建索引 1. 创建单字段索引&#xff1a; 升序索引&#xff08;默认&am…

【JAVA基础篇教学】第十四篇:Java中设计模式

博主打算从0-1讲解下java基础教学&#xff0c;今天教学第十四篇&#xff1a;Java中设计模式。 设计模式是解决软件设计中常见问题的可重复利用的解决方案。在 Java 中&#xff0c;常见的设计模式包括单例模式、工厂模式、观察者模式等。目前在基础教学篇中只展示常见的几种模…

<Halcon> 可视化窗口调用

可视化窗口调用 C#代码 if (HDevWindowStack.IsOpen()) {var hwindow HDevWindowStack.GetActive();if (hwindow null) return;HOperatorSet.SetColor(hwindow, "red");HOperatorSet.SetDraw(hwindow, "margin");HOperatorSet.DispObj(object, hwindo…

kamailio 虚变量

伪变量是以$开头的一些变量&#xff0c;这些变量可以作为参数提供给不同的脚本函数&#xff0c;并且在执行函数之前&#xff0c;它们将被替换为一个值. 伪变量的开头用字符$标记。如果你想让字符$加倍$$。 有一组预定义的伪变量&#xff0c;其名称由一个或多个字符组成&#…

【WordPress】在 Ubuntu 系统上使用 Caddy 服务器来发布 WordPress 网站

在 Ubuntu 系统上使用 Caddy 服务器来发布 WordPress 网站可以通过以下步骤完成&#xff1a; 安装 Caddy 服务器&#xff1a; 首先&#xff0c;你需要在 Ubuntu 上安装 Caddy 服务器。你可以从 Caddy 的官方网站或者仓库获取安装包&#xff0c;也可以使用以下命令进行安装&…

STM32笔记---CAN采样点设置和报错

STM32笔记---CAN采样点设置和报错 采样点设置再同步补偿宽度&#xff08;SJW&#xff09;设置 报错分析CAN中断使能寄存器CAN错误状态寄存器 采样点设置 以前配置CAN参数的BS1和BS2参数时认为总线波特率符合要求就可以了&#xff0c;其实同一个波特率可能对应多组参数设置的情…

开发公司 or 个人开发者?软件开发如何选择?

引言 随着科技的发展&#xff0c;软件开发已成为一个相对复杂的行业&#xff0c;需要专业的技能和经验来保证项目的成功。许多企业、组织和个人都纷纷加入到了软件开发的队伍中。在选择软件开发人员时&#xff0c;您可能会面临一个选择&#xff1a;是找个人开发人员还是找专业的…

访问者模式【行为模式C++】

1.概述 访问者模式是一种行为设计模式&#xff0c; 它能将算法与其所作用的对象隔离开来。 访问者模式主要解决的是数据与算法的耦合问题&#xff0c;尤其是在数据结构比较稳定&#xff0c;而算法多变的情况下。为了不污染数据本身&#xff0c;访问者会将多种算法独立归档&…