[Redis]缓存常见问题解决(缓存穿透、击穿、雪崩一文解决!通俗易懂、代码实战!手把手教你解决缓存问题三兄弟!)

news2024/7/6 19:34:28

Redis常见问题解决

要求

只用一种缓存技术,从实验点中挑一些试验进行试验原理。

1.缓存原理

目标:理解缓存的基本原理和工作机制。

实验步骤:

  1. 阅读各缓存技术机制的文档和官方资料。
  2. 实现一个简单的应用程序,模拟数据的读写和缓存操作。
  3. 观察实时操作日志,了解缓存的实际运行情况。

实验

  1. 缓存的基本原理主要围绕以下几个核心概念:

    • 时间局部性与空间局部性:缓存利用了程序访问数据的时间局部性和空间局部性,即如果一个数据被访问过,它很可能在不久的将来再次被访问;并且,如果一个数据被访问,其相邻的数据也可能很快被访问。

    • 高速存储介质:缓存通常位于更快的存储介质上,比如内存,相比硬盘等慢速存储,能显著提升数据访问速度。

    • 缓存命中与未命中:当请求的数据在缓存中存在时称为缓存命中,此时直接从缓存返回数据,无需访问较慢的后端存储;反之,如果数据不在缓存中,则称为缓存未命中,需要从后端加载数据并存储到缓存中,以便下次快速访问。

    • 缓存策略:包括但不限于LRU(Least Recently Used,最近最少使用)、LFU(Least Frequently Used,最不经常使用)和FIFO(First In First Out,先进先出)等淘汰策略,以及过期策略、缓存更新策略等。

    • 缓存一致性:在多线程或多进程环境下,确保缓存与数据源保持一致性的机制,如写直达、写回等。

  2. 以下是一个简化的Java示例,使用HashMap模拟内存缓存:

    import java.util.HashMap;
    import java.util.Map;
    
    public class SimpleCacheExample {
        private final Map<String, String> cache = new HashMap<>();
    
        public String getUserInfo(String userId) {
            // 先尝试从缓存中获取数据
            String userInfo = cache.get(userId);
            if (userInfo == null) {
                // 如果缓存中没有,模拟从数据库读取
                userInfo = fetchFromDatabase(userId);
                // 将数据放入缓存
                cache.put(userId, userInfo);
                System.out.println("缓存未命中!!!");
            } else {
                System.out.println("缓存命中!!!");
            } 
            return userInfo;
        }
    
        private String fetchFromDatabase(String userId) {
            // 这里实际应调用数据库查询逻辑,这里简单模拟
            return "UserInfo of " + userId;
        }
    
        public static void main(String[] args) {
            SimpleCacheExample cacheExample = new SimpleCacheExample();
            System.out.println(cacheExample.getUserInfo("1001")); // 首次访问,会从数据库获取
            System.out.println(cacheExample.getUserInfo("1001")); // 再次访问,直接从缓存获取
        }
    }
    
  3. 实时操作日志:

    1. 观察首次访问

      在这里插入图片描述

    2. 未命中缓存,查询数据库

      在这里插入图片描述

    3. 观察第二次访问

      在这里插入图片描述

    4. 命中缓存!直接返回结果。

      在这里插入图片描述

    5. 程序运行结果(首次访问,会从数据库获取。再次访问,直接从缓存获取。)

      在这里插入图片描述

2.缓存击穿

目标:模拟和解决缓存击穿问题。

实验步骤:

  1. 设计一个常被请求的热点数据。
  2. 启动多个并发请求同时访问该热点数据,观察缓存是否会因此而失效。
  3. 实现解决缓存击穿的方法,比如使用互斥锁或者提前加载数据。

实验

  1. 我们首先模拟实验环境(使用Redis作为缓存)

    查询心理健康产品详情接口:

    @Resource
    private RedisTemplate redisTemplate;
    int count = 1;
    
    @ApiOperation("查询心理健康产品详情")
    @GetMapping("/detail")
    public R<MentalHealthProduce> detail(@RequestParam(value = "id") String id) {
        MentalHealthProduce mentalHealthProduce;
    
        // 动态构造key
        String key = "produce_" + id; // 例:produce_1397844391040167938
    
        // 先从Redis中获取缓存数据
        mentalHealthProduce = (MentalHealthProduce) redisTemplate.opsForValue().get(key);
        // 如果存在, 直接返回, 无需查询数据库
        if (mentalHealthProduce != null) {
            return R.success(mentalHealthProduce);
        }
    
        mentalHealthProduce = mentalHealthProduceService.getById(id);
        
        // 这里每一次进行数据库查询我们进行打印日志
        System.out.println("查询数据库次数:" + count++);
        if (mentalHealthProduce == null) {
            return R.error("心理健康产品不存在");
        }
    
        // 将查询到的热点数据缓存到Redis中,过期时间为3秒
        redisTemplate.opsForValue().set(key, mentalHealthProduce, 3, TimeUnit.SECONDS);
    
        return R.success(mentalHealthProduce);
    }
    
  2. 我们进行压测,查看效果(开启10个线程,压测10秒)

    在这里插入图片描述

    我们观察控制台:即使我们开启了缓存,但在失效的时间内仍然请求了38次,在高并发中还是存在风险

    在这里插入图片描述

  3. 我们进行对缓存击穿的解决

    1. 热点数据永不过期(把过期时间取消即可,不再演示)

    2. 添加互斥锁

      1. 修改代码逻辑(加锁再去获取数据)

        // 动态构造key
        String key = "produce_" + id; // 例:produce_1397844391040167938
        // 创建基于id的唯一锁
        String lockKey = "lock_produce_" + id;
        
        // 尝试获取基于id的锁
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(); // 先加锁,确保并发安全
        
        try {
            // 先从Redis中获取缓存数据
            mentalHealthProduce = (MentalHealthProduce) redisTemplate.opsForValue().get(key);
            // 如果存在, 直接返回, 无需查询数据库
            if (mentalHealthProduce != null) {
                return R.success(mentalHealthProduce);
            }
        
            mentalHealthProduce = mentalHealthProduceService.getById(id);
        
            // 这里每一次进行数据库查询我们进行打印日志
            System.out.println("查询数据库次数:" + count++);
            if (mentalHealthProduce == null) {
                return R.error("心理健康产品不存在");
            }
        
            // 将查询到的热点数据缓存到Redis中
            redisTemplate.opsForValue().set(key, mentalHealthProduce, 3, TimeUnit.SECONDS);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 3、释放锁
            lock.unlock();
        }
        
      2. 再次进行压测:开启10个线程,压测10秒

        在这里插入图片描述

      3. 观察控制台日志:我们过期时间设置为3秒,正常来说10秒压测只会进行四次数据库查询

        在这里插入图片描述

3.缓存穿透

目标:模拟和解决缓存穿透问题。

实验步骤:

  1. 构造一个不存在于缓存和数据库中的无效数据。
  2. 启动多个并发请求同时访问该无效数据,观察缓存是否被绕过直接访问数据库。
  3. 实现解决缓存穿透的方法,比如使用布隆过滤器或者空对象缓存。

实验

  1. 我们首先模拟实验环境(使用Redis作为缓存)

    查询文章接口:

    @ApiOperation("查询文章详情")
    @GetMapping("/detail")
    public R<ArticleResp> detail(@RequestParam(value = "id") String id) {
        // 构造返回参数
        ArticleResp articleResp;
    
        // 动态构造key
        String key = "article_" + id; // 例:article_1397844391040167938
    
        // 先从Redis中获取缓存数据
        articleResp = (ArticleResp) redisTemplate.opsForValue().get(key);
        // 如果存在, 直接返回, 无需查询数据库
        if (articleResp != null) {
            return R.success(articleResp);
        }
    
        Article article = articleService.getById(id);
        if (article == null) {
            return R.error("文章不存在");
        }
    
        articleResp = new ArticleResp();
        BeanUtils.copyProperties(article, articleResp);
    
        // 设置点赞数
        int count = likeService.count(new LambdaQueryWrapper<Like>()
                                      .eq(Like::getEventId, id));
        articleResp.setLikeNum(count);
    
        // 设置评论列表
        List<CommentResp> commentRespList = commentService.list(new LambdaQueryWrapper<Comment>()
                                                                .eq(Comment::getEventId, id))
            .stream()
            .map(comment -> {
                // ......
                return commentResp;
            }).collect(Collectors.toList());
        articleResp.setCommentRespList(commentRespList);
    
        // 将查询到的文章数据缓存到Redis中
        redisTemplate.opsForValue().set(key, articleResp, 1, TimeUnit.HOURS);
    
        return R.success(articleResp);
    }
    
  2. 我们请求一个不存在的id值(id=1)来查看查询情况

    在这里插入图片描述

    可以看到,确实是请求了数据库的(缓存失效),如果黑客进行大量请求,系统将存在巨大隐患。

  3. 我们进行压测,查看效果(开启10个线程,压测10轮)

    在这里插入图片描述

  4. 我们进行对缓存穿透的解决

    1. 缓存无效数据

      • 修改代码(无效数据同样进行缓存)

        在这里插入图片描述

      • 再次进行压测:同样10个线程进行10轮压测,请求数增多(150%)的情况下平均响应时间还缩短了将近35%!!!

        在这里插入图片描述

      • 但这种处理方式是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。

    2. 布隆过滤器(Redisson实现)

      在这里插入图片描述

      1. pom.xml中导入Redisson依赖

        <!--Redisson-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.17.6</version>
        </dependency>
        
      2. RedisConfig中添加以下配置(Redis进行正常配置即可)

        @Value("${spring.redis.host}")
        private String host;
        
        @Value("${spring.redis.port}")
        private String port;
        
        @Bean
        public RedissonClient redisson() {
            //创建配置
            Config config = new Config();
            config.useSingleServer().setAddress("redis://" + host + ":" + port);
            //根据config创建出RedissonClient实例
            return Redisson.create(config);
        }
        
      3. 项目中使用

        @Resource
        private RedissonClient redissonClient;
        private RBloomFilter<String> bloomFilter;
        
        @PostConstruct // 项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法
        public void init() {
            // 启动项目时初始化bloomFilter
            List<Article> articleList = articleService.list();
            //参数:布隆过滤器的名字
            bloomFilter = redissonClient.getBloomFilter("articleFilter");
            // 初始化布隆过滤器  预计数据量   误判率
            bloomFilter.tryInit(1000L, 0.01);
            for (Article article : articleList) {
                bloomFilter.add(article.getId());
            }
        }
        

        因为我们已经初始化过数据,所以在查询时可以先查询布隆过滤器

        在这里插入图片描述

      4. 观察实际运行情况

        在这里插入图片描述

        在这里插入图片描述

4.缓存雪崩

目标:模拟和解决缓存雪崩问题。

实验步骤:

  1. 设定多个缓存数据的失效时间相同,并在某一时刻让它们同时失效。
  2. 启动多个并发请求,观察在缓存数据失效时的响应时间和Redis服务器的负载情况。
  3. 实现解决缓存雪崩的方法,比如使用不同的失效时间或者限流策略。

实验

  1. 我们首先模拟实验环境(使用Redis作为缓存)

    我们事先将全部数据存入数据库,并把所有数据存活时间都设置为60秒:

    @PostConstruct // 项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法
    public void init() {
        // 启动项目时事先存入Redis热点数据
        List<MentalHealthProduce> produceList = mentalHealthProduceService.list();
        for (MentalHealthProduce produce : produceList) {
            String key = "produce_" + produce.getId(); // 例:produce_1397844391040167938
            redisTemplate.opsForValue().set(key, produce, 60, TimeUnit.SECONDS);
        }
        System.out.println("当前时间" + new Date());
    }
    
  2. 在缓存快要失效时,我们进行压测查看效果(开启10000个线程,压测10秒)

    在这里插入图片描述

    可以看到,在缓存同一时间失效的情况下,还是会有很多请求同时请求数据库,给数据库造成一定的压力!

    在这里插入图片描述

    在这里插入图片描述

  3. 我们进行对缓存雪崩的解决

    1. 使用随机或梯度失效时间(为每项数据设置一个随机范围内的过期时间,或者采用梯度失效策略,即相近的数据项过期时间错开一定间隔),示例代码:

      Random random = new Random();
      for (String user : users) {
          long expireTime = 24 * 60 * 60 + random.nextInt(300); // 基础24小时过期时间加上0-5分钟的随机偏移
          RBucket<Object> bucket = redisson.getBucket("user:" + user);
          bucket.set(userDetails, expireTime, TimeUnit.SECONDS);
      }
      
    2. 限流与降级(在服务端实施请求限流,避免在缓存失效瞬间服务被大量请求压垮。同时,可以设置服务降级策略,当请求超过阈值时返回降级内容(如静态数据、默认值或部分数据)而非直接失败)

      1. 限流实现

        Redisson 提供了 RRateLimiter 接口来实现限流功能,支持固定窗口、滑动窗口、令牌桶等多种限流算法。例如,使用令牌桶算法进行限流,可以这样配置:

        // 创建一个令牌桶限流器,每秒填充5个令牌,最多存储10个令牌
        RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter");
        rateLimiter.trySetRate(RateType.OVERALL, 5, 10, RateIntervalUnit.SECONDS);
        
        // 在需要限流的地方尝试获取令牌,如果获取不到则说明限流
        boolean permit = rateLimiter.tryAcquire(1);
        if (!permit) {
            // 限流逻辑,比如抛出异常或者返回错误信息
        }
        
      2. 降级实现

        在限流逻辑中加入降级逻辑,当检测到请求量过大或系统资源紧张时,主动返回简化版的服务响应或者错误信息,避免进一步加重系统负担。

        例如,可以在限流失败时,执行降级逻辑:

        if (!rateLimiter.tryAcquire(1)) {
            // 降级处理,例如返回默认值或者错误提示
            return "服务繁忙,请稍后再试";
        }
        

        另外,结合Spring框架或AOP(面向切面编程),可以更加灵活地在应用层面实现更复杂的降级策略。例如,通过自定义注解和切面来判断服务是否需要降级:

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.METHOD)
        public @interface RateLimitAndFallback {
            // 可以定义一些配置属性,如降级时的返回值
        }
        
        @Aspect
        @Component
        public class RateLimitAndFallbackAspect {
        
            @Around("@annotation(rateLimitAndFallback)")
            public Object handleMethodWithRateLimit(ProceedingJoinPoint joinPoint, RateLimitAndFallback rateLimitAndFallback) throws Throwable {
                // 这里可以结合Redisson的限流逻辑
                if (!rateLimiter.tryAcquire(1)) {
                    // 根据注解或配置返回降级内容
                    return "降级响应内容";
                }
        
                // 正常执行方法
                return joinPoint.proceed();
            }
        }
        

        通过上述方式,我们可以结合Redisson的限流功能和自定义的降级策略,有效地应对高并发场景下的系统稳定性问题。

总结与优化

总结

  1. 缓存基本原理的实践揭示了缓存机制的核心优势,包括时间与空间局部性、高速存储介质的使用、命中与未命中的处理机制、多样化淘汰策略以及确保一致性的方法。通过简单的Java示例,我们直观体验了缓存对提升数据访问效率的显著作用。
  2. 缓存击穿问题通过模拟高并发访问热点数据,发现即使使用了缓存,数据过期时仍会导致数据库负载激增。采用永不过期策略和互斥锁(Mutex)解决了这一问题,有效降低了数据库压力,但需要注意永不过期策略需谨慎使用,以免造成内存压力。
  3. 缓存穿透现象通过构造不存在的数据请求,观察到直接绕过缓存访问数据库的现象。采用缓存无效数据(null值)以及布隆过滤器有效减少了无效数据库查询,提高了系统效率。布隆过滤器虽有误报可能,但在多数场景下能有效减轻数据库负担。
  4. 缓存雪崩的模拟与解决展示了当大量缓存同时过期时,系统响应时间和数据库负载的剧增。通过为数据设置随机或梯度过期时间以及实施限流与降级策略,显著改善了这一问题。限流保证了系统在高并发下的稳定性,而降级策略保证了即使在资源紧张时也能提供一定程度的服务。

优化与改进建议

  1. 精细化配置缓存策略:根据业务特点调整缓存过期时间,避免集中失效,使用梯度过期或随机过期策略分散风险。
  2. 动态调整限流阈值:根据系统实时负载动态调整限流阈值,以适应不同时间段的访问压力,避免过度限制或限制不足。
  3. 监控与预警系统:建立完善的监控体系,实时监测缓存命中率、系统负载、以及Redis服务器状态,设置阈值预警,及时发现并处理潜在问题。
  4. 优化缓存更新机制:采用异步更新策略,减少因更新缓存造成的阻塞,确保数据新鲜度的同时不影响用户体验。
  5. 分布式锁的优化:优化互斥锁的使用,减少锁等待时间,避免因锁竞争导致的性能瓶颈。探索使用读写锁或乐观锁机制,提高并发效率。
  6. 持续性能测试与调优:定期进行压力测试,模拟各种极端场景,根据测试结果不断调整和优化缓存策略和系统配置。
  7. 完善降级策略:细化降级逻辑,根据不同场景提供更合理的降级内容,如提供有限功能、默认数据或历史数据,确保用户体验。

通过上述措施,可以显著增强缓存系统性能,提高系统的稳定性和用户体验,降低运营成本。

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

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

相关文章

AD导出Gender、坐标文件、BOM

导出Gender 方式一 等输出完成后&#xff0c;将工程文件下的OutPut文件打包发给厂家即可 方式二 导出外观、层 导出孔 导出坐标文件 导出BOM 备注 外观尽量用机械层 参考 https://blog.csdn.net/lwb450921/article/details/123141335

Python发送HTML邮件有哪些步骤?怎么设置?

Python发送HTML邮件如何实现&#xff1f;Python发送邮件的策略&#xff1f; HTML邮件不仅可以包含丰富的文本格式&#xff0c;还可以插入图片、链接和其他多媒体内容&#xff0c;从而提升邮件的美观性和功能性。AokSend将详细介绍Python发送HTML邮件的主要步骤&#xff0c;帮助…

动态规划:基本概念

Dynamic Programming 动态规划&#xff08;Dynamic Programming, DP&#xff09; 是一种算法设计技巧&#xff0c;通常用来解决具有重叠子问题和最优子结构性质的问题。它通过将问题分解为更小的子问题&#xff0c;逐步解决这些子问题并将结果存储起来&#xff0c;以避免重复计…

C++拷贝构造函数、运算符重载函数、赋值运算符重载函数、前置++和后置++重载等的介绍

文章目录 前言一、拷贝构造函数1. 概念2. 特征3. 编译器生成默认拷贝构造函数4. 拷贝构造函数典型使用场景 二、运算符重载函数三、赋值运算符重载函数1. 赋值运算符重载格式2. 赋值运算符只能重载成类的成员函数不能重载成全局函数3.编译器生成一个默认赋值运算符重载4. 运算符…

上交商汤联合提出一种虚拟试穿的创新方法,利用自监督视觉变换器 (ViT) 和扩散模型

上交&商汤联合提出一种虚拟试穿的创新方法&#xff0c;利用自监督视觉变换器 (ViT) 和扩散模型&#xff0c;强调细节增强&#xff0c;通过将 ViT 生成的局部服装图像嵌入与其全局对应物进行对比。虚拟试穿体验中细节的真实感和精确度有了显着提高&#xff0c;大大超越了现有…

使用粒子滤波(particle filter)进行视频目标跟踪

虽然有许多用于目标跟踪的算法&#xff0c;包括较新的基于深度学习的算法&#xff0c;但对于这项任务&#xff0c;粒子滤波仍然是一个有趣的算法。所以在这篇文章中&#xff0c;我们将介绍视频中的目标跟踪&#xff1a;预测下一帧中物体的位置。在粒子滤波以及许多其他经典跟踪…

Antd Table 表格 拖拽列宽

antd 的表格组件的列宽&#xff0c;是通过width属性去初始化的&#xff0c;有时候渲染的内容不固定&#xff0c;这个宽做不到通用所以研究怎么实现表格列宽拖动&#xff0c;主要的实现步骤如下&#xff1a; 使用table的components API修改表格头部为 react-resizable提供的组件…

专业技能篇---计算机网络

文章目录 前言计算机网络基础一、网络分层模型 HTTP一、从输入URL到页面显示发生了什么&#xff1f;二、Http的状态码有哪些&#xff1f;三、 HTTP与HTTPS有什么区别&#xff1f;四、URI 和 URL 的区别是什么?五、Cookie和Session有什么区别&#xff1f;六、GET与POST WebSock…

基于机器学习和深度学习的C-MAPSS涡扇发动机剩余寿命RUL预测(Python,Jupyter Notebook环境)

涡扇发动机全称为涡轮风扇发动机&#xff0c;是一种先进的空中引擎&#xff0c;由涡轮喷气发动机发展而来。涡扇发动机主要特点是首级压缩机的面积比涡轮喷气发动机大。同时&#xff0c;空气螺旋桨&#xff08;扇&#xff09;将部分吸入的空气从喷射引擎喷射出来&#xff0c;并…

尚品汇-(四)

&#xff08;1&#xff09;商品的基本知识 1.1基本信息—分类 一般情况可以分为两级或者三级。咱们的项目一共分为三级&#xff0c;即一级分类、二级分类、三级分类。 比如&#xff1a;家用电器是一级分类&#xff0c;电视是二级分类&#xff0c;那么超薄电视就是三级分类。…

一单1800,这个项目凭什么这么火?

AI变现营八期学员一单1800成功拿下&#xff0c;这还是开营不到一周的结果&#xff01; AI代写这个项目为什么现在越来越火&#xff1f; 第一点原因就是因为AI的火爆&#xff0c;让传统代写行业变现效率增加了N倍&#xff0c;普通人可以入局&#xff0c;只要会调教AI就可以了&am…

Win11下安装VS2022失败的解决办法

前几天我把我的HP Z840的操作系统换成了Win11&#xff0c;在重装VS2022时遇到了麻烦&#xff0c;提示无法安装 Microsoft.VisualStudio.Devenv.Msi。 查看安装日志提示&#xff1a;Could not write value devenv.exe to key \SOFTWARE\Microsoft\Internet Explorer\Main\Featur…

基于JSP的交通事故档案管理系统

开头语&#xff1a;你好&#xff0c;我是计算机学长猫哥&#xff0c;如果你对系统有更多的期待或建议&#xff0c;欢迎随时联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSPJava 工具&#xff1a;ECLIPSE、Tomcat 系统展示 首页 管理员界…

基于YOLOv5的火灾检测系统的设计与实现

基于YOLOv5的火灾检测系统的设计与实现 概述系统架构主要组件代码结构功能描述YOLOv5检测器视频处理器主窗口 详细代码说明YOLOv5检测器类视频处理类主窗口类 使用说明环境配置运行程序操作步骤 检测示例图像检测视频检测实时检测 数据集介绍数据集获取数据集规模 YOLOv5模型介…

vscode中同一页面使用批量替换

在vscode中像word中那样批量替换 首先搜索要替换的内容快捷键是ctrlf 然后输入你要搜索的内容 第二个框中输入你要替换成的内容 点击全部替换&#xff0c;就可以了

Web应用和Tomcat的集成鉴权1-BasicAuthentication

作者:私语茶馆 1.Web应用与Tomcat的集成式鉴权 Web应用部署在Tomcat时,一般有三层鉴权: (1)操作系统鉴权 (2)Tomcat容器层鉴权 (3)应用层鉴权 操作系统层鉴权包括但不限于:Tomcat可以和Windows的域鉴权集成,这个适合企业级的统一管理。也可以在Tomcat和应用层独立…

高级算法入门必看—21个NPC问题及其证明

文章目录 前言一、布尔可满足性问题二、每子句至多3个变量的布尔可满足性问题&#xff08;3-SAT&#xff09;三、0-1整数规划&#xff08;0-1 integer programming&#xff09;四、Set packing&#xff08;Set packing&#xff09;五、最小顶点覆盖问题&#xff08;Vertex cove…

计算机视觉 | 基于图像处理和边缘检测算法的黄豆计数实验

目录 一、实验原理二、实验步骤1. 图像读取与预处理2. 边缘检测3. 轮廓检测4. 标记轮廓序号 三、实验结果 Hi&#xff0c;大家好&#xff0c;我是半亩花海。 本实验旨在利用 Python 和 OpenCV 库&#xff0c;通过图像处理和边缘检测算法实现黄豆图像的自动识别和计数&#xff0…

港湾周评|高盛眼中的618增长

《港湾商业观察》李镭 年中最重要的购物节618终于尘埃落定了。2024年的618各大电商平台竞技情况如何&#xff1f;又有哪些新的亮点&#xff1f;都成为外界观察消费行为的参考指标。 根据京东618数据显示&#xff1a;累计成交额过10亿的品牌83个&#xff0c;超15万个中小商家销…

python watchdog 配置文件热更新

目录 一、Watchdog示例 二、aiohttp服务配置热更新 在同事的golang代码中学习到了config.json热更新的功能&#xff0c;这里自己也学习了一下python写web服务的时候怎么来实现配置的热更新。主要是利用Watchdog这个第三方python库&#xff0c;来监控文件系统的改变&#xff0…