Redis分布式锁存在的问题及解决方案(值得珍藏)

news2024/9/28 13:16:03

Redis分布式锁存在的问题

在购票软件的情境中,当仅剩一张或几张票时,众多用户同时尝试购买。在不考虑任何外部干扰的情况下,逻辑上,系统应首先检查是否还有余票。如果仍有余票,用户可以顺利购买并导致库存相应减少;如果没有余票,系统会提示用户票数不足,购买失败。伪代码如下:

void buyTicket() {
    int stockNum = byTicketMapper.selectStockNum();
    if(stockNum>0){
        //TODO 买票流程....
        byTicketMapper.reduceStock(); // 扣减库存
    }else{
        log.info("=====>票卖完了<====");
    }
}

这段代码在逻辑上没有问题,但是在并发场景下,可能会存在一个严重的问题。当剩余票数为1时,有A,B两个用户同时点击了购买按钮,A用户通过了库存大于0的校验并开始执行购票逻辑,但是由于一些原因造成A用户的购票线程有短暂的阻塞。而在这个阻塞的过程中,用户B发起了购买请求,并且也通过了库存大于0的校验,直到整个购买流程执行完成并且扣减了库存。那么这个时候剩余库存刚好为0,不会再有用户发起购买请求,这时用户A的购买请求阻塞被唤醒,因为在此之前已经校验过库存大于0,所以执行完购买流程后,库存还会被扣减一次。那么此时的库存为-1,这就是常听到的超卖问题。

在这里插入图片描述

为了避免这个问题,我们可以通过加锁了方式,来保证并发的安全性。像JVM提供的内置锁synchronized,JUC提供的重入锁ReentrantLock,但是这两种锁只能保证单机环境下并发安全问题,一般在实际工作中很少会部署单节点的项目,通常都是多节点集群部署,这两个锁就失去了意义。这个时候就可以借助redis来实现分布式锁。

插播一条:如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供

setnx

在集群部署的情况下,通常使用redis来实现分布式锁。其中redis提供了setnx命令,标识只有key不存在时才能设值成功,从而达到加锁的效果。下面通过redis来改造上述的代码,其方式是购票线程首先获取锁,如果获取锁成功,那么继续执行购票业务流程,直到所有流程执行完成并扣减库存后,最终在释放锁。如果获取锁失败,那么就给出一个友好的系统提示。

void buyTicket() {
    // 获取锁
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
    if (lock) {
        int stockNum = byTicketMapper.selectStockNum();
        if(stockNum>0){
            //TODO 买票流程....
            byTicketMapper.reduceStock(); // 扣减库存
        }else{
            log.info("=====>票卖完了<====");
        }
        // 释放锁
        redisTemplate.delete("lock");
    } else {
        log.info("=====>系统繁忙,请稍后!<====");
    }
}

问题1:死锁问题

通过上面的一顿梭哈,你以为这样就可以了吗,其实不然。设想一下,如果线程A在获取锁成功后,在执行购票的逻辑中出现了异常,那么这个时候就会造成锁得不到释放,其他线程始终获取不到锁,这就造成严重的死锁问题。为了避免死锁问题的出现,我们可以对异常进行捕获,在finally中去释放锁,这样不管业务执行成功或失败,最后都会去释放锁。

void buyTicket() {
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
    if (lock) {
        try {
            int stockNum = byTicketMapper.selectStockNum();
            if (stockNum > 0) {
                //TODO 买票流程....
                byTicketMapper.reduceStock(); // 扣减库存
            } else {
                log.info("=====>票卖完了<====");
            }
        }finally {
            redisTemplate.delete("lock");   // 释放锁
        }
    } else {
        log.info("=====>系统繁忙,请稍后!<====");
    }
}

你以为这就结束了吗?死锁就不会发生了吗?如果你认为这样就能避免死锁的发生,那你就太不细心啦。如果在程序刚想像执行释放锁的逻辑时,redis服务突然宕机了,那么这时锁释放就失败了。在将redis服务重启后,加锁的数据又被恢复了,这样又出现了死锁的现象。为了避免这个问题,可以为锁设置一个过期时间,这样即使redis重启恢复数据后,也会很快的过期掉。不过需要注意的是,在设置锁的过期时间时,一定要保证原子性操作,不然还是会出现死锁问题。

插播一条:如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供

//不是原子操作,会出现死锁问题
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
//如果刚要执行该语句时,redis宕机了。上面的锁无法释放
redisTemplate.expire("lock",Duration.ofSeconds(5L));

//原子操作
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1", Duration.ofSeconds(5L));

问题2:锁被其他线程释放问题

经过上面的又一顿梭哈,死锁的问题可以避免了,这样在高并发的情况下就能安全的执行了吗。如果锁的过期时间设置了5秒,当A线程发起购票请求并获取到了锁,但是A线程在执行购票流程时花费了6秒,此时线程A的锁已经过期。这时线程B重新获取了锁并且也开始执行购票流程,但是A线程要比B线程执行的要快,当A线程释放锁时,问题就出现了。由于A线程执行的过程锁已经过期了,那么在执行释放锁的流程时,最终被释放的是线程B的锁,这就导致B的锁被A线程释放问题。
在这里插入图片描述

对于这个现象,可以给每个锁设置一个唯一标识,比如像UUID,线程ID。在释放锁时,校验一下这个锁的标识是否为需要删除的锁,如果是,在进行锁的释放。

public void buyTicket() {
    String uuid = UUID.randomUUID().toString();
    // 为锁设置一个唯一标识
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, Duration.ofSeconds(5L));
    if (lock) {
        try {
            int stockNum = byTicketMapper.selectStockNum();
            if (stockNum > 0) {
                //TODO 买票流程....
                byTicketMapper.reduceStock(); // 扣减库存
            } else {
                log.info("=====>票卖完了<====");
            }
        }finally {
            String lockValue = redisTemplate.opsForValue().get("lock");
            if(lockValue.equals(uuid)){ //校验标识,通过则释放锁
                redisTemplate.delete("lock");   
            }
        }
    } else {
        log.info("=====>系统繁忙,请稍后!<====");
    }
}

问题3:锁续期问题

使用setnx命令做分布式锁时,无法避免的一个问题就是:线程尚未执行完成,但是锁已经过期。在解决锁被其他线程误删的代码中,并不是100%能解决的,问题点在于下面这段代码。如果线程A已经执行到了if语句并且通过了判断,当刚要执行释放锁的逻辑时,线程A的锁过期了并且线程B重新获取到了锁,那么线程A在释放锁时,释放的是B的锁。为了完全能够解决这个问题,可以采用锁续期的方式,其实现方式是单独开一个线程用来定时监听线程的锁是否还被持有,如果还持有,那么就给这把锁增加一些过期时间,这样就不会出现上述问题了。目前市面上已经为我们提供了锁自动续期的中间件,比如redisson

插播一条:如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供

 String lockValue = redisTemplate.opsForValue().get("lock");
  if(lockValue.equals(uuid)){ // 线程A的锁过期
      redisTemplate.delete("lock");   // 线程A删除了线程B的锁
   }

Redisson

redisson一般使用最多的场景就是分布式锁了,它不仅保证了并发场景下线程安全的问题,也解决了锁续期的问题。使用方式也比较简单,以3.5.7版本为例,首先需要配置redisson信息,根据自己的redis集群模式自由选择配置。在配置完成后,再来改造上面的购票方法。

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 单机配置
        config.useSingleServer().setAddress("redis://127.0.0.1:3306").setDatabase(0);
        // 主从配置
        // config.useMasterSlaveServers().setMasterAddress("").addSlaveAddress("","");
        // 哨兵配置
        // config.useSentinelServers().addSentinelAddress("").setMasterName("");
        // Cluster配置
        //config.useClusterServers().addNodeAddress("");
        return Redisson.create(config);
    }

对于redisson使用起来也非常简单,通过getLock方法获取到RLock对象。通过RLock的tryLock或lock方法来进行加锁(底层都是通过Lua脚本来实现的)。当获取到锁并且扣减库存后,可以使用unlock方法进行锁释放。

void buyTicket() {
    RLock lock = redissonClient.getLock("lock");
    if (lock.tryLock()) {  // 获取锁
        try {
            int stockNum = byTicketMapper.selectStockNum();
            if (stockNum > 0) {
                //TODO 买票流程....
                byTicketMapper.reduceStock(); // 扣减库存
            } else {
                log.info("=====>票卖完了<====");
            }
        } finally {
            lock.unlock(); //释放锁
        }
    } else {
        log.info("=====>系统繁忙,请稍后!<====");
    }
}
  • Watch Dog机制

那redisson是如何做到锁续期的呢?其实在redisson内部有一个看watch dog机制(看门狗机制),但是看门狗机制并不是在加锁时就能启动的。需要注意的是在加锁时,如果使用tryLock(long t1,long t2, TimeUnit unit)或lock(long t1,long t2, TimeUnit unit)方法并且将t2参数值设为了一个不为-1的值,那么看门口将无法生效。看门狗在启动后会监听主线程还在执行,如果还在执行那么将会通过Lua脚本每10秒给锁续期30秒。watchlog的延时时间默认为30秒,这个值可以在配置config时自己定义。

private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1L) { // 如果leaseTime不是-1,那么将无法使用看门狗
        return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    } else {
        RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        ttlRemainingFuture.addListener(new FutureListener<Boolean>() {
            public void operationComplete(Future<Boolean> future) throws Exception {
                if (future.isSuccess()) {
                    Boolean ttlRemaining = (Boolean)future.getNow();
                    if (ttlRemaining) {
                        // 看门口机制
                        RedissonLock.this.scheduleExpirationRenewal(threadId);
                    }

                }
            }
        });
        return ttlRemainingFuture;
    }
}
private long lockWatchdogTimeout = 30000L; //默认30秒
private void scheduleExpirationRenewal(final long threadId) {
    if (!expirationRenewalMap.containsKey(this.getEntryName())) {
        // 每10秒执行续期
        Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            public void run(Timeout timeout) throws Exception {
            // 通过LUA脚本为锁续期
                RFuture<Boolean> future = RedissonLock.this.commandExecutor.evalWriteAsync(RedissonLock.this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(RedissonLock.this.getName()), new Object[]{RedissonLock.this.internalLockLeaseTime, RedissonLock.this.getLockName(threadId)});
                future.addListener(new FutureListener<Boolean>() {
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        RedissonLock.expirationRenewalMap.remove(RedissonLock.this.getEntryName());
                        if (!future.isSuccess()) {
                            RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", future.cause());
                        } else {
                            if ((Boolean)future.getNow()) {
                                RedissonLock.this.scheduleExpirationRenewal(threadId);
                            }

                        }
                    }
                });
            }
        }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS); // 每10秒执行一次
        if (expirationRenewalMap.putIfAbsent(this.getEntryName(), task) != null) {
            task.cancel();
        }

    }
}

问题4:主从切换导致锁丢失问题

虽然redisson帮助我们解决了锁续期的问题,但是在redis集群架构中,由于主从复制具有一定的延时,那么在极端情况下就会出现这样一个问题:当一个线程获取锁成功,并且成功向主节点保存了锁信息,当主节点还未像从节点同步锁信息时,主节点宕机了,那么当发生故障转移从节点切换为主节点时,线程加的锁就丢失了。为了解决这个问题,redis引入了红锁RedLock,RedLock与大多数中间件的选举机制类似,采用过半的方式来决定操作成功还是不成功。

RedLock

  • 加锁

RedLock在工作中,并不接受redis的集群架构,无论是主从,哨兵还是Cluster。每台redis服务都是独立的,都是一台独立的Master节点。在加锁的过程中,RedLock会记录开始加锁时的时间以及加锁成功后的时间,这两个时间差就是一台机器加锁成功所需要的时间。比如启动了5个redis服务,线程A设置锁的超时时间为5秒,当像第一台redis服务加锁成功后花费了1秒,像第二台服务加锁成功后也花费了一秒。这个时候加到第二台机器时,已经花费了两秒的时间,但是加锁数并未过半,还需要加锁一台才能完全算加锁成功,这个时候第三台机器加锁成功又花费了1秒。那么总的加锁时间就是3秒,锁的实际过期时间就为2秒。特别需要注意的是,在向redis服务建立网络连接时,要设置一个超时时间,避免redis服务宕机时,客户端还在傻傻的等待回应,这里超时时间官方给到建议是5-50毫秒之间,当连接超时时,客户端会继续向下一个节点发起连接。

在这里插入图片描述

加锁失败

如果因为某些原因,获取锁失败(加锁没有超半数或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,即便某些Redis实例根本就没有加锁成功。

插播一条:如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供

失败重试

在并发场景下,RedLock会出现这样一个问题,比如有三个线程同时去获取了同一张票的锁,此时A线程已经成功给redis-1和reids-2加上了锁,线程B已经成功给redis-3,reids-4加上了锁,线程C成功的给reids-5加上了锁,这个时候三个线程再去加锁时,没有机器可加了,发现加锁成功数都未过半,那么就导致客户端始终获取不到锁。

在这里插入图片描述

当客户端无法取到锁时,应该在随机延迟一定时间,然后进行重试,防止多个客户端在同时抢夺同一资源的锁。

释放锁

释放锁比较简单,向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.

在了解了RedLock后,最后再来改造购票的代码逻辑。首先需要根据redis的实例数来定义对应的Bean实例,redis的实例最少要有三台。

插播一条:如果你近期准备面试跳槽,建议在ddkk.com在线刷题,涵盖 一万+ 道 Java 面试题,几乎覆盖了所有主流技术面试题,还有市面上最全的技术五百套,精品系列教程,免费提供

@Bean
public RedissonClient redissonClient() {
    Config config = new Config();
    // 单机配置
    config.useSingleServer().setAddress("redis://192.168.36.128:3306").setDatabase(0);
    return Redisson.create(config);
}

@Bean
public RedissonClient redissonClient2() {
    Config config = new Config();
    // 单机配置
    config.useSingleServer().setAddress("redis://192.168.36.130:3306").setDatabase(0);
    return Redisson.create(config);
}

@Bean
public RedissonClient redissonClient3() {
    Config config = new Config();
    // 单机配置
    config.useSingleServer().setAddress("redis://192.168.36.131:3306").setDatabase(0);
    return Redisson.create(config);
}

在配置完成后,为每台实例都设置同一把锁,最后在调用RedissonRedLock提供的tryLock和unlock进行加锁和解锁。

void buyTicket(){
    RLock lock = redissonClient.getLock("lock");
    RLock lock2 = redissonClient2.getLock("lock");
    RLock lock3 = redissonClient3.getLock("lock");
    RedissonRedLock redLock = new RedissonRedLock(lock,lock2,lock3); // 分别像三台实例加锁
    if (redLock.tryLock()) {
        try {
            int stockNum = byTicketMapper.selectStockNum();
            if (stockNum > 0) {
                //TODO 买票流程....
                byTicketMapper.reduceStock(); // 扣减库存
            } else {
                log.info("=====>票卖完了<====");
            }
        } finally {
            redLock.unlock();  //释放锁
        }
    } else {
        log.info("=====>系统繁忙,请稍后!<====");
    }
}

总结

在使用reids做分布式锁时,并没有想象中的那么简单,高并发场景下容易出现死锁,锁被其他线程误删,锁续期,锁丢失等问题,在实际开发中应该考虑到这些问题并根据相应的解决办法来解决这些问题,从而保证系统的安全性。本文中可能会存在一些遗漏或错误,后续会继续跟进。
log.info(“=>票卖完了<”);
}
} finally {
redLock.unlock(); //释放锁
}
} else {
log.info(“=>系统繁忙,请稍后!<”);
}
}


# 总结

在使用reids做分布式锁时,并没有想象中的那么简单,高并发场景下容易出现死锁,锁被其他线程误删,锁续期,锁丢失等问题,在实际开发中应该考虑到这些问题并根据相应的解决办法来解决这些问题,从而保证系统的安全性。本文中可能会存在一些遗漏或错误,后续会继续跟进。

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

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

相关文章

鸿蒙next开发-OpenHarmony的NDK开发

Native API&#xff08;NDK&#xff09;入门 Native API是OpenHarmony SDK上提供的一组native开发接口与工具集合&#xff08;也称为NDK&#xff09;&#xff0c;方便开发者使用C或者C语言实现应用的关键功能。Native API只覆盖了OHOS基础的一些底层能力&#xff0c;如libc&am…

史上最全EasyExcel

一、EasyExcel介绍 1、数据导入&#xff1a;减轻录入工作量 2、数据导出&#xff1a;统计信息归档 3、数据传输&#xff1a;异构系统之间数据传输 二、EasyExcel特点 Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内…

制作一个Python聊天机器人

我们学习一下如何使用 ChatterBot 库在 Python 中创建聊天机器人&#xff0c;该库实现了各种机器学习算法来生成响应对话&#xff0c;还是挺不错的 什么是聊天机器人 聊天机器人也称为聊天机器人、机器人、人工代理等&#xff0c;基本上是由人工智能驱动的软件程序&#xff0…

代码随想录训练营第三十天|332.重新安排行程51. N皇后37. 解数独

332.重新安排行程 采用哈希表方式进行储存出发点和到达点&#xff0c;终止条件是result中的机场数量为机票1&#xff1b; 重点在于哈希表的运用和遍历方法 51. N皇后 wtf? 回溯算法非常形象的N叉树解法&#xff1a; 输入n&#xff0c;row col,chessboard 终止条件&#x…

2024年pmp的考试时间是什么时候?

2024最新考试时间已经出来了&#xff1a;分别是 3月、6月、8月、11月&#xff0c;四次&#xff0c;具体考试日期还需要关注官网的进一步通知。 一、PMP报考条件 年龄满足 22 周岁有官方授权的培训机构给的 35个PDU&#xff08;学时&#xff09; 就能报名。 是不是相当于没有…

[Python] 如何通过ctypes库来调用C++ 动态库 DLL?

ctypes库介绍 ctypes是Python的一个外部库,它提供了一种灵活的方式来调用C语言的动态链接库(DLL)或共享库(SO)。通过ctypes,我们可以在Python中直接调用C语言编写的函数和变量,从而实现跨语言的互操作。 ctypes 它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的…

本地MinIO存储服务通过Java程序结合Cpolar内网穿透进行远程连接

文章目录 前言1. 创建Buckets和Access Keys2. Linux 安装Cpolar3. 创建连接MinIO服务公网地址4. 远程调用MinIO服务小结5. 固定连接TCP公网地址6. 固定地址连接测试 前言 MinIO是一款高性能、分布式的对象存储系统&#xff0c;它可以100%的运行在标准硬件上&#xff0c;即X86等…

【Linux系列】在Pop!OS的启动器中添加自定义程序图标

文章目录 前言一、创建快捷方式二、快捷方式参数三、添加右键菜单和注册MIME 前言 无论是在Windows上&#xff0c;还是Linux&#xff0c;或者安卓这些我们常用的操作系统上&#xff0c;一些应用程序的快捷方式放在桌面或者启动器&#xff0c;只需要简单的点击就可以启动&#…

【算法】串联所有单词的子串【滑动窗口】

题目 给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。例如&#xff0c;如果 words ["ab","cd","ef"]&#xff0c; 那么 "abcd…

Unity 编辑器篇|(十一)Gizmos (全面总结 | 建议收藏)

目录 1. 前言2 参数总览3 Gizmos绘制3.1 立方体&#xff1a;DrawCube3.2 视锥&#xff1a;DrawFrustum3.3 贴图&#xff1a;DrawGUITexture3.4 图标&#xff1a;DrawIcon3.5 线段&#xff1a;DrawLine3.6 网格&#xff1a;DrawMesh3.7 射线&#xff1a;DrawRay3.8 球体&#xf…

PBR材质背光面太暗优化

图形学中漫反射光照遵循兰伯特光照模型&#xff0c;它的公式如下 其中&#xff1a; &#xff1a;漫反射光颜色 &#xff1a;入射光颜色 &#xff1a;材质的漫反射系数 &#xff1a;法线方向 &#xff1a;光源方向 由于背光面的法线方向和光源方向的点积为负数&#xff0c;因此…

为什么 Golang Fasthttp 选择使用 slice 而非 map 存储请求数据

文章目录 Slice vs Map&#xff1a;基本概念内存分配和性能Fasthttp 中的 SliceMap性能优化的深层原因HTTP Headers 的特性CPU 预加载特性 结论 Fasthttp 是一个高性能的 Golang HTTP 框架&#xff0c;它在设计上做了许多优化以提高性能。其中一个显著的设计选择是使用 slice 而…

golang 中使用 statik 将静态资源编译进二进制文件中

现在的很多程序都会提供一个 Dashboard 类似的页面用于查看程序状态并进行一些管理的功能&#xff0c;通常都不会很复杂&#xff0c;但是其中用到的图片和网页的一些静态资源&#xff0c;如果需要用户额外存放在一个目录&#xff0c;也不是很方便&#xff0c;如果能打包进程序发…

shell编程-3

文章目录 shell学习第三天while 循环第一天的小游戏练习: 编写抽同学回答问题的脚本要想让这个脚本永久有效如何知道两个文件里的内存一样&#xff1f;如何判断某个人已经抽过了 文本处理相关命令seqxargsuniqsorttrcutawkpastesplitcoljoin小结一下作业 小知识点写脚本的流程怎…

基于SkyEye仿真飞腾处理器:运行U-Boot并加载Phytium-FreeRTOS

仿真平台在帮助提升研发效率、加快产品面市时间上的作用已得到诸多验证&#xff0c;通过对处理器进行仿真来支持嵌入式系统及软件的虚拟化开发、测试和验证成为目前应用较为广泛的方法。天目全数字实时仿真软件SkyEye是一款基于可视化建模的硬件行为级仿真平台&#xff0c;在众…

找免费PPT素材、模板,就上这6个网站。

这6个PPT模板网站&#xff0c;免费下载&#xff0c;可商用&#xff0c;赶紧来下载。 1、菜鸟图库 https://www.sucai999.com/search/ppt/0_0_0_1.html?vNTYxMjky 菜鸟图库是一个设计、办公、媒体等素材非常齐全的网站&#xff0c;站内有几百万的庞大素材库&#xff0c;其中PP…

Python 异源mesh裁剪融合实现与优化

Python 异源mesh裁剪融合实现与优化 一、项目需求二、解决方案1. 代码2. 结果3. 耗时 三、优化探索0. 分析1. 在体素边界处进行裁剪2. 用mesh分块进行裁剪3. 用缓冲区的思路裁剪 一、项目需求 对mesh进行裁剪&#xff0c;但发现若非mesh是致密的&#xff0c;那么裁剪边会出现锯…

用通俗易懂的方式讲解:使用Llama-2、PgVector和LlamaIndex,构建大模型 RAG 全流程

近年来&#xff0c;大型语言模型&#xff08;LLM&#xff09;取得了显著的进步&#xff0c;然而大模型缺点之一是幻觉问题&#xff0c;即“一本正经的胡说八道”。其中RAG&#xff08;Retrieval Augmented Generation&#xff0c;检索增强生成&#xff09;是解决幻觉比较有效的…

Linux网络引导自动安装centos7

目录 一、部署PXE远程安装服务 1. 系统装机的三种引导方式 2. pxe概述 3. 实现过程 4. 搭建过程中服务介绍 4.1 TFTP服务 4.2 vsftp&#xff1a;安装系统镜像文件获取方式 4.3 syslinux 4.4 DHCP服务 5. 操作过程 二、实现Kickstart无人值守安装 1. 安装Kickstart图…

vue项目执行依赖安装(npm i或npm install )报ls-remote -h -t异常

从git拉取的vue项目执行依赖安装时一直报错&#xff0c; 报错如下图&#xff1a;首先&#xff0c;查看了node版本、npm配置的镜像地址均没找到解决办法。 在命令行中直接输入git发现提示于是从网上搜到了一个博文https://blog.csdn.net/weixin_49159364/article/details/118198…