限流浅解(漏斗-令牌)

        限流是指对系统或应用的访问流量进行调整或限制的过程,以维护系统的可用性和稳定性。是为了保护系统在高负载或者异常情况下不被过多的请求压垮,在实际应用中,限流通常是通过控制一定时间内的请求数量或请求速率来实现的。它可以帮助我们解决一些常见问题,例如:

  1. 防止恶意攻击:恶意发起大量的请求可能会占用服务器的大量资源,导致系统崩溃、宕机等严重问题。

  2. 保障服务稳定性:如果系统中某个接口或领域服务出现问题,可能会导致整个系统出现问题,限制访问量可以减轻压力,使系统能够更好地承受压力。

  3. 节约成本:如果没有限流,那么在系统访问量过高的情况下,我们不得不增加硬件资源来支撑更大的访问峰值,但是这将导致成本的大幅增加。

  4. 保障系统核心功能或关键任务的优先运行,防止非必要的请求对重要业务造成干扰;

  5. 热门产品秒杀场景实现等等

常见的限流策略包括令牌桶、漏桶等方法。令牌桶算法是指在一定时间内,将流量限制为一定速率,速率可通过令牌桶的大小进行调整。漏桶算法则是指在一定时间内,限制流的速度,离散的请求被拉成等速率的连续请求。除此之外,还有一些其他的限流策略,例如基于QPS的流量控制、基于线程池的方案、基于信号量的限制等。

维护系统的可用性和稳定性除了限流,还有熔断,服务降级等具体来说:

熔断:

熔断指的是一种服务保护机制,在分布式系统中应用广泛。其作用是在出现服务异常时,自动切断请求,以避免服务继续处理导致更大范围的故障发生。具体来说,熔断通常实现以下功能:

1.监控服务调用失败率、延迟等指标;

2.当异常情况达到阈值时,熔断器自动打开,阻止请求进入后端服务;

扫描二维码关注公众号,回复: 16063915 查看本文章

3.定期或根据条件自动尝试恢复服务状态,逐渐放行请求以达到正常服务状态。

服务降级:

服务降级指的是在系统高负载、资源紧张或异常情况下,通过限制某些服务功能或调整服务等级,来保证核心业务可用性和性能。具体而言,服务降级通常涉及以下方面:

1.选择一些非关键路径、低优先级或少用的服务进行停用、缩减功能或减少质量等级;

2.通过流量控制、错误码响应等手段提示客户端请求已被处理但不返回完整结果;

3.制定相应的策略和规则,以方便系统动态调整服务并达到最佳效果。

限流的算法

限流算法很多,常见的有三类,分别是计数器算法、漏桶算法、令牌桶算法。

计数器算法又分为固定窗口计数器算法和滑动窗口计数器算法。简单说下这几种算法

固定窗口计数器算法是一种简单且常用的限流算法,其基本思想是在固定时间窗口内计数请求的数量,当请求的数量超过阈值时拒绝请求。

固定窗口计数器算法的应用场景如下:

  1. API 接口的并发请求控制:对于一些需要保证并发请求量不超过一定值的 API 接口,可以使用固定窗口计数器算法进行限流控制。

  2. 数据库访问控制:对于一些需要对数据库访问进行限制的场景,可以使用固定窗口计数器算法限制每秒访问的次数。

  3. 消息队列消费者控制:对于一些需要对消息队列消费者进行控制的场景,可以使用固定窗口计数器算法限制每秒处理的消息数。

需要注意的是,固定窗口计数器算法存在的问题是无法平滑处理请求,可能会出现流量波动较大、存在突发流量难以处理等问题。因此,在实际应用中需要结合具体业务场景,综合考虑选择合适的限流算法。

滑动窗口计数器算法是一种比固定窗口计数器算法更加精细的限流算法,它不仅在固定时间窗口内计数请求的数量,还可以对时间窗口进行滑动,从而更加精准地控制请求流量。

滑动窗口计数器算法的应用场景如下:

  1. API 接口的 QPS(每秒查询数)控制:对于一些需要保证 API 接口每秒查询数不超过一定值的场景,可以使用滑动窗口计数器算法进行限流控制。

  2. 网络流量控制:对于一些需要限制网络流量的场景,可以使用滑动窗口计数器算法限制每秒的网络流量。

  3. 分布式系统流量控制:对于一些需要对分布式系统流量进行控制的场景,可以使用滑动窗口计数器算法进行流量控制,从而保证系统的稳定性和可靠性。

需要注意的是,滑动窗口计数器算法相比固定窗口计数器算法更加精细,但也需要更加复杂的实现和计算,因此在实际应用中需要根据具体的业务场景和系统架构进行选择和调整。同时,滑动窗口计数器算法也需要注意对窗口大小的合理设置,以保证限流效果的良好表现。

漏桶算法是一种常用的流量控制算法,它基于一个固定大小的桶来限制流量。这个桶以固定的速率漏水,如果流量过大,则会积累到桶中,一旦桶满了,多余的流量将被直接丢弃。

漏桶算法的主要思想是将流量看作水流,桶看作一个容器,流量的速率就相当于水流进入容器的速率,而容器的大小就相当于桶的大小,水流从容器底部漏出,漏出的速率就是固定的速率。它主要适用于一些需要平滑控制流量的场景,例如网络限速、流媒体缓冲等。漏桶算法可以保证输出数据的速率是固定的,并且在流量突发的情况下也能够有效控制流量,避免网络拥塞和系统崩溃。在实际应用中,漏桶算法通常用于对请求进行限流控制,例如网关、API服务、负载均衡等场景。通过设置固定的速率和桶的大小,可以控制请求的流量,并避免系统被过多的请求拖垮。

总之,漏桶算法是一种常用的流量控制算法,适用于需要平滑控制流量的场景,可以保证系统稳定性和可靠性。在实际应用中,漏桶算法可以用于对请求进行限流控制,避免系统过载和崩溃。

令牌桶算法也是一种常用的流量控制算法,它与漏桶算法不同,令牌桶算法是基于令牌桶的方式对请求进行限流控制。令牌桶算法的基本思想是,系统会以一定的速率生成令牌,而每个请求需要消耗一个令牌,如果当前没有令牌,则无法通过请求。

令牌桶算法的主要优点是可以平滑地控制请求的流量,以一定的速率生成令牌,保证每个请求都有足够的令牌可以消耗,从而实现流量控制的目的。同时,令牌桶算法还可以通过调整令牌生成的速率和令牌桶的大小来控制请求的流量,以满足不同的业务需求。

令牌桶算法适用于一些需要平滑控制流量的场景,例如API服务、网关、负载均衡、消息队列等。通过令牌桶算法可以实现对请求进行限流控制,防止系统过载和崩溃,同时也可以保证系统的可靠性和稳定性。

总之,令牌桶算法是一种常用的流量控制算法,适用于需要平滑控制流量的场景,可以实现对请求进行限流控制,避免系统过载和崩溃,保证系统的可靠性和稳定性。

代码 固定窗口计数器算法限流

import java.util.concurrent.TimeUnit;

public class RateLimiter {
    private int limit; // 限流阈值
    private long interval; // 时间窗口
    private long lastTime; // 上一次通过的时间
    private int count; // 当前窗口中已通过的数量

    public RateLimiter(int limit, long interval) {
        this.limit = limit;
        this.interval = interval;
        this.lastTime = System.currentTimeMillis();
        this.count = 0;
    }

    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();

        if (currentTime >= lastTime + interval) { // 超过时间窗口,重置计数器
            lastTime = currentTime;
            count = 0;
        }

        if (count < limit) { // 当前通过的数量未超过限流阈值
            count++;
            return true;
        }

        return false; // 超过限流阈值,拒绝通过
    }

    public static void main(String[] args) throws InterruptedException {
        RateLimiter limiter = new RateLimiter(5, TimeUnit.SECONDS.toMillis(1));

        for (int i = 0; i < 20; i++) {
            Thread.sleep(200);

            if (limiter.tryAcquire()) {
                System.out.println("通过");
            } else {
                System.out.println("拒绝");
            }
        }
    }
}

在上面的示例中,RateLimiter 类封装了固定窗口计数器算法限流的实现。在构造函数中,传入了限流阈值和时间窗口,初始化了上一次通过的时间和当前窗口中已通过的数量。

tryAcquire 方法用于尝试获取令牌,实现了固定窗口计数器算法的逻辑。首先获取当前时间,如果当前时间超过了上一次通过的时间加上时间窗口,就重置计数器,然后将当前时间作为上一次通过的时间。然后判断当前通过的数量是否未超过限流阈值,如果未超过,就将当前通过的数量加 1,并返回 true 表示通过。否则返回 false 表示拒绝通过。

在 main 方法中,创建了一个 RateLimiter 对象,并循环 20 次调用 tryAcquire 方法进行限流测试。每次调用之间间隔 200 毫秒,如果获取到了令牌,就输出“通过”,否则输出“拒绝”。

滑动窗口计数器算法限流

import java.util.concurrent.TimeUnit;

public class SlidingWindowRateLimiter {
    private int limit; // 限流阈值
    private long interval; // 时间窗口
    private long[] timestamps; // 记录每个时间窗口通过的数量
    private int index; // 当前时间窗口的索引
    private int count; // 当前时间窗口中已通过的数量

    public SlidingWindowRateLimiter(int limit, long interval) {
        this.limit = limit;
        this.interval = interval;
        this.timestamps = new long[(int) (interval / 1000)];
        this.index = 0;
        this.count = 0;
    }

    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        int currentIndex = (int) (currentTime / 1000 % interval / 1000); // 计算当前时间窗口的索引

        if (currentIndex != index) { // 当前时间窗口与记录的时间窗口不一致,重置计数器
            timestamps[currentIndex] = 0;
            index = currentIndex;
            count = 0;
        }

        if (count < limit) { // 当前通过的数量未超过限流阈值
            timestamps[index] = currentTime; // 记录通过的时间戳
            count++;
            return true;
        }

        long oldestTimestamp = timestamps[index + 1 == timestamps.length ? 0 : index + 1]; // 获取最早的时间戳
        if (currentTime - oldestTimestamp <= interval) { // 时间窗口内通过的数量已经达到限流阈值
            return false; // 拒绝通过
        }

        timestamps[index] = currentTime; // 更新通过的时间戳
        index = index + 1 == timestamps.length ? 0 : index + 1; // 更新当前时间窗口的索引
        count = 1; // 更新当前时间窗口中已通过的数量
        return true;
    }

    public static void main(String[] args) throws InterruptedException {
        SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(5, TimeUnit.SECONDS.toMillis(1));

        for (int i = 0; i < 20; i++) {
            Thread.sleep(200);

            if (limiter.tryAcquire()) {
                System.out.println("通过");
            } else {
                System.out.println("拒绝");
            }
        }
    }
}

在上面的示例中,SlidingWindowRateLimiter 类封装了滑动窗口计数器算法限流的实现。在构造函数中,传入了限流阈值和时间窗口,初始化了记录每个时间窗口通过的数量的 timestamps 数组,当前时间窗口的索引和当前时间窗口中已通过的数量。

tryAcquire 方法用于尝试获取令牌,实现了滑动窗口计数器算法的逻辑。首先获取当前时间,并计算出当前时间窗口的索引。如果当前时间窗口与记录的时间窗口不一致,就重置计数器。然后判断当前通过的数量是否未超过限流阈

滑动窗口计数器算法限流和固定窗口计数器算法限流是两种常见的限流算法。

固定窗口计数器算法的实现比较简单,它将时间窗口固定为固定的时间段,比如一秒钟,然后在每个时间窗口内记录通过的请求数量。当请求到来时,先检查当前时间窗口内通过的请求数量是否超过了限流阈值,如果超过了就拒绝请求,否则就通过请求,并将当前请求计入当前时间窗口的请求数量。这种算法的缺点是,如果在一个时间窗口内请求量过多,容易导致拒绝服务攻击。

滑动窗口计数器算法则可以解决固定窗口计数器算法的缺点。它将时间窗口分成多个小的时间段,并记录每个小时间段内通过的请求数量。每个小时间段的长度通常是固定的,比如1秒钟。当请求到来时,先检查当前时间窗口内通过的请求数量是否超过了限流阈值,如果超过了就拒绝请求,否则就通过请求,并将当前请求计入当前时间窗口的请求数量。同时,还需要判断当前时间窗口内通过的请求数量是否已经超过了限流阈值,如果超过了就需要移动时间窗口,删除最早的时间段,并将当前时间段添加到时间窗口中。这种算法的优点是能够更加平滑地限制流量,防止拒绝服务攻击,但实现比固定窗口计数器算法要稍微复杂一些。

总的来说,固定窗口计数器算法的实现简单,适用于处理请求量比较稳定的情况,而滑动窗口计数器算法的实现稍微复杂一些,但可以更加平滑地限制流量,防止拒绝服务攻击,适用于处理请求量比较不稳定的情况。

令牌桶算法限流

import java.util.concurrent.*;

public class TokenBucket {
    // 定义一个定时调度器,用于定期生成令牌
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    // 令牌桶的队列,用于存储令牌
    private final BlockingQueue<Object> tokenBucket = new ArrayBlockingQueue<>(1);
    // 令牌桶的容量和令牌的生成间隔时间
    private final int refillIntervalMs;
    private final int capacity;

    // 构造函数,初始化令牌桶的容量、令牌生成间隔和令牌桶初始值,并开始生成令牌
    public TokenBucket(int tokensPerInterval, int intervalMs, int capacity) {
        // 将令牌桶初始值添加到队列中
        tokenBucket.add(new Object());
        this.capacity = capacity;
        // 计算令牌生成间隔时间
        this.refillIntervalMs = intervalMs / tokensPerInterval;
        // 定期调用令牌生成方法
        scheduler.scheduleAtFixedRate(() -> refill(), refillIntervalMs, refillIntervalMs, TimeUnit.MILLISECONDS);
    }

    // 尝试从令牌桶中获取一个令牌
    public boolean tryConsume() {
        // 从队列中获取一个令牌,如果队列为空,则返回null
        return tokenBucket.poll() != null;
    }

    // 生成一个令牌并添加到令牌桶中
    private void refill() {
        // 对令牌桶进行同步操作
        synchronized (tokenBucket) {
            // 如果队列中的令牌数量小于令牌桶的容量,则添加一个新的令牌
            if (tokenBucket.size() < capacity) {
                tokenBucket.add(new Object());
            }
        }
    }
}

在此示例中,我们使用ScheduledExecutorService来周期性地添加令牌。 tokenBucket是一个大小为1的阻塞队列,该队列用于存储令牌。在构造函数中,我们设置了每个时间间隔要添加的令牌数量,以及桶的容量。使用scheduler.scheduleAtFixedRate()方法来定期调用refill()方法,该方法在tokenBucket队列中添加令牌。 tryConsume()方法用于尝试从tokenBucket队列中获取令牌。

public static void main(String[] args) {
    TokenBucket tokenBucket = new TokenBucket(10, 1000, 20);
    for (int i = 0; i < 100; i++) {
        boolean allowed = tokenBucket.tryConsume();
        System.out.println("Request " + i + " " + (allowed ? "succeeded" : "failed"));
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在此代码段中,我们创建了一个令牌桶,该令牌桶每秒添加10个令牌,最大容量为20。然后,我们进行100次循环,并在每次循环中尝试获取令牌。我们还添加了一个短暂的线程休眠以模拟请求之间的延迟。您应该会看到输出中的类似以下信息的消息:

Request 0 succeeded
Request 1 succeeded
Request 2 succeeded
...
Request 18 failed
Request 19 succeeded
...
Request 36 failed
Request 37 succeeded
...

漏斗算法限流

import java.util.concurrent.*;

public class LeakyBucket {
    // 定义一个定时调度器
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    // 漏桶的队列,用于存储令牌
    private final BlockingQueue<Object> leakyBucket = new ArrayBlockingQueue<>(1);
    // 漏桶的容量和漏速率
    private final int capacity;
    private final int leakRateMs;

    // 构造函数,初始化漏桶的容量和漏速率,并开始漏水
    public LeakyBucket(int capacity, int leakRateMs) {
        this.capacity = capacity;
        this.leakRateMs = leakRateMs;
        // 定期调用漏水方法
        scheduler.scheduleAtFixedRate(() -> leak(), leakRateMs, leakRateMs, TimeUnit.MILLISECONDS);
    }

    // 尝试从漏桶中获取一个令牌
    public boolean tryConsume() {
        return leakyBucket.poll() != null;
    }

    // 添加一个令牌到漏桶中
    public void addWater() {
        synchronized (leakyBucket) {
            // 如果漏桶还没有满,则添加一个令牌到漏桶中
            if (leakyBucket.size() < capacity) {
                leakyBucket.add(new Object());
            }
        }
    }

    // 漏水方法,从漏桶中移除一个令牌
    private void leak() {
        synchronized (leakyBucket) {
            // 如果漏桶不为空,则移除一个令牌
            if (leakyBucket.size() > 0) {
                leakyBucket.remove();
            }
        }
    }
}

在此示例中,我们使用ScheduledExecutorService来周期性地移除漏桶中的令牌。 leakyBucket是一个大小为1的阻塞队列,用于存储令牌。在构造函数中,我们设置了漏桶的容量和漏速率。使用scheduler.scheduleAtFixedRate()方法来定期调用leak()方法,该方法从leakyBucket队列中移除令牌。 tryConsume()方法用于尝试从leakyBucket队列中获取令牌,而addWater()方法用于将令牌添加到漏桶中。

使用以下代码段,您可以测试此类是否有效

public static void main(String[] args) {
    LeakyBucket leakyBucket = new LeakyBucket(10, 1000);
    for (int i = 0; i < 100; i++) {
        leakyBucket.addWater();
        boolean allowed = leakyBucket.tryConsume();
        System.out.println("Request " + i + " " + (allowed ? "succeeded" : "failed"));
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在此代码段中,我们创建了一个漏桶,该漏桶最大容量为10,漏速率为1000毫秒/个。然后,我们进行100次循环,并在每次循环中尝试获取令牌。我们还添加了一个短暂的线程休眠以模拟请求之间的延迟。您应该会看到输出中的类似以下信息的消息

Request 0 succeeded
Request 1 succeeded
Request 2 succeeded
...
Request 18 failed
Request 19 succeeded
...
Request 36 failed
Request 37 succeeded
...

这表明限流算法正在有效地控制令牌的流量。

猜你喜欢

转载自blog.csdn.net/qq_20714801/article/details/130202086