每日一博 - 漫谈流控小妙招

news2025/2/25 14:23:11

文章目录

  • Pre
  • 概述
  • 实现方式
    • 固定窗口算法(计数器法)
      • 定义
      • 实现
    • 滑动窗口算法
      • 定义
      • 实现
    • 漏桶算法
      • 定义
      • 实现
    • 令牌桶算法
      • 定义
      • 实现
        • 每秒产生5个令牌
        • 应对突发流量
        • 平滑预热
    • 网关限流
    • 流量整形中间件限流

在这里插入图片描述


Pre

深入理解分布式技术 - 限流

并发编程-25 高并发处理手段之消息队列思路 + 应用拆分思路 + 应用限流思路

SpringBoot - 优雅的实现【流控】


概述

限流作为一种流量控制策略 (通常会和熔断、降级搭配在一起使用,来避免瞬时的大量请求对系统造成负荷,来达到保护服务平稳运行的目的)旨在维护系统的稳定性。然而,限流也带来了平衡用户满意度与系统稳定性之间的挑战。

限流策略的核心意义:
限流是一种管理流量的方法,通过设置最大请求率或并发连接数,以防止系统被过多请求压垮。随着数字服务的普及,限流变得尤为重要,因为高流量可能导致系统崩溃,影响整体可用性。

限流与用户体验的平衡:
限流策略的挑战之一是如何在保持系统稳定的同时提供令人满意的用户体验。过于严格的限流可能导致用户等待时间过长,降低用户满意度。但没有限流,系统可能会被过度请求而崩溃,影响用户所有人。

我们这里将深入探讨限流的集中实现方式

实现方式

固定窗口算法(计数器法)

定义

在这里插入图片描述

固定窗口算法通过在单位时间内维护一个计数器,能够限制在每个固定的时间段内请求通过的次数,以达到限流的效果。

实现

 @Slf4j
public class FixedWindowRateLimiter {
    // 时间窗口大小,单位毫秒
    private long windowSize;
    // 允许通过请求数
    private int maxRequestCount;

    // 当前窗口通过的请求计数
    private AtomicInteger count = new AtomicInteger(0);
    // 窗口右边界
    private long windowBorder;

    public FixedWindowRateLimiter(long windowSize, int maxRequestCount) {
        this.windowSize = windowSize;
        this.maxRequestCount = maxRequestCount;
        windowBorder = System.currentTimeMillis() + windowSize;
    }

    /**
     * 尝试获取许可
     *
     * @return 若获取成功则返回 true,否则返回 false
     */
    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();

        // 如果窗口边界小于当前时间,则表示窗口重置
        if (windowBorder < currentTime) {
            log.info("窗口重置");
            // 重新计算新的窗口边界,确保窗口大小不变
            do {
                windowBorder += windowSize;
            } while (windowBorder < currentTime);
            // 重置计数器
            count = new AtomicInteger(0);
        }

        // 如果请求计数小于允许的最大请求数
        if (count.intValue() < maxRequestCount) {
            log.info("获取许可成功");
            // 计数增加并返回成功
            count.incrementAndGet();
            return true;
        } else {
            log.info("获取许可失败");
            return false;
        }
    }
}

通过构造方法中的参数指定时间窗口大小以及允许通过的请求数量,当请求进入时先比较当前时间是否超过窗口上边界,未越界且未超过计数器上限则可以放行请求。

测试

    public static void main(String[] args) throws InterruptedException {
       // 在1000毫秒内通过5个请求
        FixedWindowRateLimiter fixedWindowRateLimiter  = new FixedWindowRateLimiter(1000, 5);

        for (int i = 0; i < 10; i++) {
            if (fixedWindowRateLimiter.tryAcquire()) {
                System.out.println("执行任务");
            }else{
                System.out.println("被限流");
                TimeUnit.MILLISECONDS.sleep(300);
            }
        }
    }

在这里插入图片描述

缺点 :临界问题

假设有一个恶意用户,在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在1秒里面,瞬间发送了200个请求。我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。

刚才的问题其实是因为我们统计的精度太低。那么如何很好地处理这个问题呢?或者说,如何将临界问题的影响降低呢?我们可以看下面的滑动窗口算法


滑动窗口算法

定义

在这里插入图片描述

滑动窗口算法在固定窗口的基础上,进行了一定的升级改造。它的算法的核心在于将时间窗口进行了更精细的分片,将固定窗口分为多个小块,每次仅滑动一小块的时间。

并且在每个时间段内都维护了单独的计数器,每次滑动时,都减去前一个时间块内的请求数量,并再添加一个新的时间块到末尾,当时间窗口内所有小时间块的计数器之和超过了请求阈值时,就会触发限流操作。

实现

该算法的实现,核心就是通过一个int类型的数组循环使用来维护每个时间片内独立的计数器:

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SlidingWindowRateLimiter {
    // 时间窗口大小,单位毫秒
    private long windowSize;
    // 分片窗口数
    private int shardNum;
    // 允许通过请求数
    private int maxRequestCount;
    // 各个窗口内请求计数
    private int[] shardRequestCount;
    // 请求总数
    private int totalCount;
    // 当前窗口下标
    private int shardId;
    // 每个小窗口大小,毫秒
    private long tinyWindowSize;
    // 窗口右边界
    private long windowBorder;

    public SlidingWindowRateLimiter(long windowSize, int shardNum, int maxRequestCount) {
        this.windowSize = windowSize;
        this.shardNum = shardNum;
        this.maxRequestCount = maxRequestCount;
        shardRequestCount = new int[shardNum];
        tinyWindowSize = windowSize / shardNum;
        windowBorder = System.currentTimeMillis();
    }

    /**
     * 尝试获取许可
     *
     * @return 若获取成功则返回 true,否则返回 false
     */
    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        
        // 如果当前时间超过窗口右边界,进行窗口滑动操作
        if (currentTime > windowBorder) {
            do {
                shardId = (++shardId) % shardNum;
                totalCount -= shardRequestCount[shardId];
                shardRequestCount[shardId] = 0;
                windowBorder += tinyWindowSize;
            } while (windowBorder < currentTime);
        }

        // 如果请求总数小于允许的最大请求数
        if (totalCount < maxRequestCount) {
            log.info("获取许可成功。窗口ID:{}", shardId);
            shardRequestCount[shardId]++;
            totalCount++;
            return true;
        } else {
            log.info("获取许可失败。窗口ID:{}", shardId);
            return false;
        }
    }
}

测试

// 对第一个例子中的规则进行修改,每1秒允许100个请求通过不变,在此基础上再把每1秒等分为10个0.1秒的窗口。
 SlidingWindowRateLimiter slidingWindowRateLimiter
                = new SlidingWindowRateLimiter(1000, 10, 10);
        TimeUnit.MILLISECONDS.sleep(800);

        for (int i = 0; i < 15; i++) {
            boolean acquire = slidingWindowRateLimiter.tryAcquire();
            if (acquire){
                System.out.println("执行任务");
            }else{
                System.out.println("被限流");
            }
            TimeUnit.MILLISECONDS.sleep(10);
        }

在这里插入图片描述

程序启动后,在先休眠了一段时间后再发起请求,可以看到在0.9秒到1秒的时间窗口内放行了6个请求,在1秒到1.1秒内放行了4个请求,随后就进行了限流,解决了在固定窗口算法中相邻时间窗口内允许通过大量请求的问题

滑动窗口算法通过将时间片进行分片,对流量的控制更加精细化,但是相应的也会浪费一些存储空间,用来维护每一块时间内的单独计数,并且还没有解决固定窗口中可能出现的流量激增问题。


漏桶算法

定义

为了应对流量激增的问题,后续又衍生出了漏桶算法,用专业一点的词来说,漏桶算法能够进行流量整形和流量控制

漏桶是一个很形象的比喻,外部请求就像是水一样不断注入水桶中,而水桶已经设置好了最大出水速率,漏桶会以这个速率匀速放行请求,而当水超过桶的最大容量后则被丢弃。

在这里插入图片描述

实现

@Slf4j
public class LeakyBucketRateLimiter {
    // 桶的容量
    private int capacity;
    // 桶中现存水量
    private AtomicInteger water = new AtomicInteger(0);
    // 开始漏水时间
    private long leakTimeStamp;
    // 水流出的速率,即每秒允许通过的请求数
    private int leakRate;

    public LeakyBucketRateLimiter(int capacity, int leakRate) {
        this.capacity = capacity;
        this.leakRate = leakRate;
    }

    /**
     * 尝试获取许可
     *
     * @return 若获取成功则返回 true,否则返回 false
     */
    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();

        // 如果桶中没有水,重新开始计算
        if (water.get() == 0) {
            log.info("开始漏水");
            leakTimeStamp = currentTime;
            water.incrementAndGet();
            return water.get() < capacity;
        }

        // 先漏水,计算已漏水量
        int leakedWater = (int) ((currentTime - leakTimeStamp) / 1000 * leakRate);
        log.info("上次漏水时间:{}, 当前时间:{}. 已漏水量:{}", leakTimeStamp, currentTime, leakedWater);

        // 如果已漏水量不为0,更新桶中水量
        if (leakedWater != 0) {
            int leftWater = water.get() - leakedWater;
            // 可能水已漏光,设为0
            water.set(Math.max(0, leftWater));
            leakTimeStamp = currentTime;
        }
        log.info("剩余容量:{}", capacity - water.get());

        // 如果桶还有容量,获取许可
        if (water.get() < capacity) {
            log.info("获取许可成功");
            water.incrementAndGet();
            return true;
        } else {
            log.info("获取许可失败");
            return false;
        }
    }
}

测试

 LeakyBucketRateLimiter leakyBucketRateLimiter
                = new LeakyBucketRateLimiter(3, 1);
        for (int i = 0; i < 15; i++) {
            if (leakyBucketRateLimiter.tryAcquire()) {
                System.out.println("执行任务");
            } else {
                System.out.println("被限流");
            }
            TimeUnit.MILLISECONDS.sleep(500);
        }

先初始化一个漏桶,设置桶的容量为3,每秒放行1个请求,在代码中每500毫秒尝试请求1次

09:01:58.404 [main] INFO com.artisan.LeakyBucketRateLimiter - start leaking
执行任务
09:01:58.911 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530118410, currentTime:1693530118911. LeakedWater:0
09:01:58.914 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:2
09:01:58.915 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:01:59.422 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530118410, currentTime:1693530119422. LeakedWater:1
09:01:59.422 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:2
09:01:59.422 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:01:59.934 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530119422, currentTime:1693530119934. LeakedWater:0
09:01:59.934 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:1
09:01:59.934 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:02:00.444 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530119422, currentTime:1693530120444. LeakedWater:1
09:02:00.444 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:1
09:02:00.444 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:02:00.976 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530120444, currentTime:1693530120975. LeakedWater:0
09:02:00.976 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:0
09:02:00.976 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire fail
被限流
09:02:01.489 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530120444, currentTime:1693530121489. LeakedWater:1
09:02:01.490 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:1
09:02:01.490 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:02:02.002 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530121490, currentTime:1693530122002. LeakedWater:0
09:02:02.002 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:0
09:02:02.002 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire fail
被限流
09:02:02.515 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530121490, currentTime:1693530122515. LeakedWater:1
09:02:02.515 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:1
09:02:02.515 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:02:03.016 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530122515, currentTime:1693530123016. LeakedWater:0
09:02:03.016 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:0
09:02:03.016 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire fail
被限流
09:02:03.531 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530122515, currentTime:1693530123531. LeakedWater:1
09:02:03.531 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:1
09:02:03.531 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:02:04.042 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530123531, currentTime:1693530124042. LeakedWater:0
09:02:04.042 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:0
09:02:04.042 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire fail
被限流
09:02:04.553 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530123531, currentTime:1693530124553. LeakedWater:1
09:02:04.553 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:1
09:02:04.553 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务
09:02:05.066 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530124553, currentTime:1693530125066. LeakedWater:0
09:02:05.066 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:0
09:02:05.066 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire fail
被限流
09:02:05.580 [main] INFO com.artisan.LeakyBucketRateLimiter - lastTime:1693530124553, currentTime:1693530125580. LeakedWater:1
09:02:05.580 [main] INFO com.artisan.LeakyBucketRateLimiter - 剩余容量:1
09:02:05.580 [main] INFO com.artisan.LeakyBucketRateLimiter - tryAcquire success
执行任务

Process finished with exit code 0

漏桶算法的缺点,不管当前系统的负载压力如何,所有请求都得进行排队,即使此时服务器的负载处于相对空闲的状态,这样会造成系统资源的浪费。由于漏桶的缺陷比较明显,所以在实际业务场景中,使用的比较少。


令牌桶算法

定义

令牌桶算法是基于漏桶算法的一种改进,主要在于令牌桶算法能够在限制服务调用的平均速率的同时,还能够允许一定程度内的突发调用。

它的主要思想是系统以恒定的速度生成令牌,并将令牌放入令牌桶中,当令牌桶中满了的时候,再向其中放入的令牌就会被丢弃。而每次请求进入时,必须从令牌桶中获取一个令牌,如果没有获取到令牌则被限流拒绝。
在这里插入图片描述

假设令牌的生成速度是每秒100个,并且第一秒内只使用了70个令牌,那么在第二秒可用的令牌数量就变成了130,在允许的请求范围上限内,扩大了请求的速率。当然,这里要设置桶容量的上限,避免超出系统能够承载的最大请求数量。

实现

Guava中的RateLimiter就是基于令牌桶实现的

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

每秒产生5个令牌

 void acquireTest(){
        RateLimiter rateLimiter=RateLimiter.create(5);
        for (int i = 0; i < 10; i++) {
            double time = rateLimiter.acquire();
            log.info("等待时间:{}s",time);
        }
    }

  

在这里插入图片描述
可以看到 每200ms左右产生一个令牌并放行请求,也就是1秒放行5个请求,使用RateLimiter能够很好的实现单机的限流。


应对突发流量

前面提到的突发流量情况,令牌桶是怎么解决的呢?RateLimiter中引入了一个预消费 的概念。

在这里插入图片描述

翻译一下:

申请令牌的数量 不同不会影响这个申请令牌这个动作本身的响应时间,acquire(1)和acquire(1000)这两个请求会消耗同样的时间返回结果,但是会影响下一个请求的响应时间。

如果一个消耗大量令牌的任务到达空闲 的RateLimiter,会被立即批准执行,但是当下一个请求进来时,将会额外等待一段时间,用来支付前一个请求的时间成本。

举个例子: 当一个系统处于空闲状态时,突然来了1个需要消耗100个令牌的任务,那么白白等待100秒是毫无意义的浪费资源行为,那么可以先允许它执行,并对后续请求进行限流时间上的延长,以此来达到一个应对突发流量的效果。

   void acquireMultiTest(){
        RateLimiter rateLimiter=RateLimiter.create(1);

        for (int i = 0; i <3; i++) {
            int num = 2 * i + 1;
            log.info("获取{}个令牌", num);
            double cost = rateLimiter.acquire(num);
            log.info("获取{}个令牌结束,耗时{}s",num,cost);
        }
    }

    

在这里插入图片描述

可以看到,在第二次请求时需要3个令牌,但是并没有等3秒后才获取成功,而是在等第一次的1个令牌所需要的1秒偿还后,立即获得了3个令牌得到了放行。

同样,第三次获取5个令牌时等待的3秒是偿还的第二次获取令牌的时间,偿还完成后立即获取5个新令牌,而并没有等待全部重新生成完成。

平滑预热

RateLimiter还具有平滑预热功能,下面的代码就实现了在启动3秒内,平滑提高令牌发放速率到每秒5个的功能

在这里插入图片描述

 

    void acquireSmoothly(){
        RateLimiter rateLimiter =RateLimiter.create(5,3, TimeUnit.SECONDS);
        long startTimeStamp = System.currentTimeMillis();
        for (int i = 0; i < 15; i++) {
            double time = rateLimiter.acquire();
            log.info("等待时间:{}s, 总时间:{}ms" ,time,System.currentTimeMillis()-startTimeStamp);
        }
    }

在这里插入图片描述

可以看到,令牌发放时间从最开始的500ms多逐渐缩短,在3秒后达到了200ms左右的匀速发放。

网关限流

https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html/#the-requestratelimiter-gatewayfilter-factory

https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html/#the-redis-ratelimiter

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


流量整形中间件限流

https://sentinelguard.io/zh-cn/docs/introduction.html

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

Revit SDK:AutoParameter 添加参数

前言 这个例子介绍如果往族文件里添加参数。 内容 Revit 的参数&#xff0c;参考官方文档&#xff1a; 这个例子的关键接口&#xff1a; // 通过 FamilyManager 添加参数 FamilyParameter AddParameter(string parameterName, BuiltInParameterGroup parameterGroup, Categ…

【JS案例】JS实现积分抽奖(内附源码)

JS案例实现积分抽奖 &#x1f31f;效果展示 &#x1f31f;HTML结构 &#x1f31f;CSS样式 &#x1f31f;实现思路 &#x1f31f;具体实现 1.定义抽奖次数渲染 2.点击抽奖按钮,实现滚动抽奖效果 3.弹窗处理 &#x1f31f;完整代码 &#x1f31f;写在最后 &#x1f3…

阿里面试经验分享:从被回绝到Offer,详解应聘阿里技术岗位注意事项

本文是《谈谈应聘阿里全流程》的姊妹篇&#xff0c;《谈谈应聘阿里全流程》发布后&#xff0c;收到了很多读者的积极反馈&#xff0c;但其中也反映出读者普遍的困惑&#xff1a;清楚了应聘流程&#xff0c;该如何有针对性地做应聘准备呢&#xff1f;这个问题从正面并不好回答&a…

windows自带远程桌面连接的正确使用姿势

摘要 目前远程办公场景日趋广泛&#xff0c;对远程控制的需求也更加多样化&#xff0c;windows系统自带了远程桌面控制&#xff0c;在局域网内可以实现流程的远程桌面访问及控制。互联网使用远程桌面则通常需要使用arp等内网穿透软件&#xff0c;市场上teamviewer、Todesk、向…

浅探Android 逆向前景趋势~

前段时间&#xff0c;我和朋友偶然间谈起安卓逆向&#xff0c;他问我安卓逆向具体是什么&#xff0c;能给我们带来什么实质性的东西&#xff0c;我也和朋友大概的说了一下&#xff0c;今天在这里拿出来和大家讨论讨论&#xff0c;也希望帮助大家来了解安卓逆向。 谈起安卓逆向…

C++面试题(叁)---操作系统篇

目录 操作系统篇 1 Linux中查看进程运行状态的指令、查看内存使用情况的指令、 tar解压文件的参数。 2 文件权限怎么修改 3 说说常用的Linux命令 4 说说如何以root权限运行某个程序。 5 说说软链接和硬链接的区别。 6 说说静态库和动态库怎么制作及如何使用&#xff0c;区…

sql:SQL优化知识点记录(六)

&#xff08;1&#xff09;索引优化1 查看一下有没有建立索引&#xff1a; 用到索引中的一个&#xff1a;type中的ref决定访问性能 用到索引中的两个&#xff1a;通过key_len的长度可以看出来&#xff0c;比第一个大一点。或者通过ref&#xff1a;中用到了两个常量const 用到了…

ubuntu系统安装QQ音乐

前言 要问程序员除了编程软件最离不开的最重要的是什么软件&#xff0c;个人觉得就是音乐软件了&#xff0c;所以QQ音乐单独拿出来整理一下&#xff0c;其他的软件放在一起整理一下。“一起长大的约定&#xff0c;那样真心&#xff0c;与你聊不完的曾经&#xff0c;而我已经分…

图像扭曲之万花筒

源码&#xff1a; void kaleidoscope(cv::Mat& src,cv::Mat& dst,double angle,double radius) {dst.create(src.rows, src.cols, CV_8UC3);dst.setTo(0);int cx src.cols / 2;int cy src.rows / 2;//angle PI / 4;double angle2 PI / 4;double sides radius / 3…

【板栗糖GIS】——360浏览器的下载图标隐藏在内部不方便,怎么修改

目录 1. 设置前的本来样子 2. 登录360的皮肤中心 3. 使用se13的经典皮肤 最近edge浏览器最近使用bilibili和notion都非常卡&#xff0c;时不时崩溃&#xff0c;不得不换浏览器使用&#xff0c;试来试去360浏览器最得我心&#xff0c;只不过广告太多&#xff0c;调教也是花了…

复杂性分析与算法设计:解锁计算机科学的奥秘

文章目录 算法复杂性分析的基本概念时间复杂度空间复杂度 常见的算法设计策略1. 分治法2. 贪心法3. 动态规划 算法设计的实际应用1. 网络路由2. 图像处理3. 人工智能 算法的选择和性能分析结论 &#x1f389;欢迎来到数据结构学习专栏~复杂性分析与算法设计&#xff1a;解锁计算…

excel 无法删除有合并单元格的列内容时的替代方法

背景&#xff1a; hp 笔记本电脑&#xff1b;win10 64位&#xff1b;excel 版本 16.0&#xff1b; office 2016自带excel 问题&#xff1a; 把pdf转excel后&#xff0c;由于原 pdf 图表本身的原因&#xff0c;转换后有不规则合并单元格的现象。 而在选择某列进行“删除” &a…

完整开发实现公众号主动消息推送,精彩内容即刻到达

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月CSDN上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

【爬虫】实验项目三:验证码处理与识别

目录 一、实验目的 二、实验预习提示 三、实验内容 实验要求 基本要求&#xff1a; 改进要求A&#xff1a; 改进要求B&#xff1a; 四、实验过程 基本要求 五、源码如下 六、资料 一、实验目的 部分网站可能会使用验证机制来阻止用户无效登录或者是验证用户不是用程…

vue竖向步骤条

效果图&#xff1a; 弹框组件代码&#xff1a; <template><el-dialog:visible.sync"dialogVisible":append-to-body"true":close-on-click-modal"false":close-on-press-escape"false"titlewidth"8.2rem"custom-c…

Redis 缓存穿透、击穿、雪崩

一、缓存穿透 1、含义 缓存穿透是指查询一个缓存中和数据库中都不存在的数据&#xff0c;导致每次查询这条数据都会透过缓存&#xff0c;直接查库&#xff0c;最后返回空。 2、解决方案 1&#xff09;缓存空对象 就是当数据库中查不到数据的时候&#xff0c;我缓存一个空对象…

力扣:82. 删除排序链表中的重复元素 II(Python3)

题目&#xff1a; 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - …

[NSSCTF 2nd] 2023 web方向和misc方向题解 wp

WEB php签到 直接给了源码。 是一个文件上传题目。分析一下源码。 <?phpfunction waf($filename){//黑名单$black_list array("ph", "htaccess", "ini");//得到文件后缀&#xff0c;【有漏洞】$ext pathinfo($filename, PATHINFO_EXTEN…

CData Drivers for SAS xpt Crack

CData Drivers for SAS xpt Crack 使用基于标准的驱动程序&#xff0c;加入数据库、报告工具和自定义程序中的实时SAS xpt(XPORT)数据文件。 与BI分析、报告、ETL工具和自定义解决方案集成。 适用于SAS xpt的CData驱动程序。神奇的功能&#xff1a; BI和分析 我们的驱动程序是将…

VCRUNTIME140_1.dll丢失是怎么回事?有哪些解决方法

今天&#xff0c;我这里与大家分享一个关于VCRUNTIME140_1.dll丢失修复的经验。相信很多网友在日常使用电脑的过程中&#xff0c;都会遇到这样的问题&#xff1a;程序无法运行&#xff0c;提示“缺少VCRUNTIME140_1.dll”的错误。那么&#xff0c;VCRUNTIME140_1.dll丢失是怎么…