限流是什么?怎么限流?
  n3y4rZ8GUfmO 2023年12月23日 54 0


1.为什么需要限流?

限流(Rate Limiting)是一种控制数据或服务访问速率的技术,通常用于防止滥用、保护系统免受过载和攻击,以及确保服务质量。通过限制访问速率,限流可以确保系统的稳定性和可靠性,并防止单个用户或来源过度消耗资源。

限流的基本原理是在一定时间段内限制对特定资源或服务的请求速率。这可以通过设置阈值来实现,例如每秒允许的最大请求数。当请求速率超过阈值时,系统可以采取不同的策略来处理额外的请求,例如拒绝请求、延迟处理或限制请求的频率。

下面是一个简单的例子来帮助你理解限流的概念:

就像去⾃助餐厅吃饭,如果有⼈⼀股脑地把所有美⻝都拿光了,其他⼈就⽆法享用。所以,我们需要对此做出一些限制,以便其他用户正常享用。

2.限流算法

🍊固定窗口限流

固定窗口限流算法是一种最简单的限流算法,其原理是在固定时间窗口(单位时间)内限制请求的数量。该算法将时间分成固定的窗口,并在每个窗口内限制请求的数量。具体来说,算法将请求按照时间顺序放入时间窗口中,并计算该时间窗口内的请求数量,如果请求数量超出了限制,则拒绝该请求。假设单位时间是1秒,限流阀值为3。在单位时间1秒内,每来一个请求,计数器就加1,如果计数器累加的次数超过限流阀值3,后续的请求全部拒绝。等到1秒结束后,计数器清0,重新开始计数。

限流是什么?怎么限流?_限流

缺点:可出现流量突刺

限定:1 ⼩时只允许 10 个⽤户操作

⽐如:前 59 分钟没有 1 个操作,第 59 分钟来了 10 个操作;第 1 ⼩时 01 分钟⼜来 了 10 个操作。相当于 2 分钟内执⾏了 20 个操作,服务器仍然有⾼峰危险。

/**
     * 固定窗口时间算法
     * @return
     */
    boolean fixedWindowsTryAcquire() {
        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        if (currentTime - lastRequestTime > windowUnit) {  //检查是否在时间窗口内
            counter = 0;  // 计数器清0
            lastRequestTime = currentTime;  //开启新的时间窗口
        }
        if (counter < threshold) {  // 小于阀值
            counter++;  //计数器加1
            return true;
        }

        return false;
    }

🍒滑动窗口限流算法

滑动窗口限流算法是一种更为复杂的限流算法,它解决了固定窗口限流算法存在的问题。在滑动窗口限流算法中,时间窗口不再是固定的,而是根据请求的到达时间动态滑动。算法将时间划分为多个小窗口,每个窗口有一定的计数器,用于记录该窗口内的请求数量。当请求到达时,根据请求的时间戳确定其所属的窗口,并将计数器加1。如果某个窗口内的请求数量超过了设定的阈值,则后续的请求将被拒绝或限制。

优点:能够解决上述流量突刺的问题,因为第 59 分钟时,限流窗口是 59 分 ~1 小时 59 分,这个时间段内只能接受 10 次请求,只要还在这个窗⼝内,更多的操作就会被拒绝。

缺点:实现相对固定窗⼝来说⽐较复杂,限流效果和你的滑动单位有关,过大的时间窗口可能导致无法及时处理突发流量,而过小的时间窗口可能导致频繁地滑动窗口和重置计数器,增加系统的开销。因此,在实际应用中需要进行充分的测试和调优来确定合适的时间窗口大小和滑动步长。

/**
     * 单位时间划分的小周期(单位时间是1分钟,10s一个小格子窗口,一共6个格子)
     */
    private int SUB_CYCLE = 10;

    /**
     * 每分钟限流请求数
     */
    private int thresholdPerMin = 100;

    /**
     * 计数器, k-为当前窗口的开始时间值秒,value为当前窗口的计数
     */
    private final TreeMap<Long, Integer> counters = new TreeMap<>();

   /**
     * 滑动窗口时间算法实现
     */
    boolean slidingWindowsTryAcquire() {
        long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / SUB_CYCLE * SUB_CYCLE; //获取当前时间在哪个小周期窗口
        int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数

        //超过阀值限流
        if (currentWindowNum >= thresholdPerMin) {
            return false;
        }

        //计数器+1
        counters.get(currentWindowTime)++;
        return true;
    }

   /**
    * 统计当前窗口的请求数
    */
    private int countCurrentWindow(long currentWindowTime) {
        //计算窗口开始位置
        long startTime = currentWindowTime - SUB_CYCLE* (60s/SUB_CYCLE-1);
        int count = 0;

        //遍历存储的计数器
        Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Long, Integer> entry = iterator.next();
            // 删除无效过期的子窗口计数器
            if (entry.getKey() < startTime) {
                iterator.remove();
            } else {
                //累加当前窗口的所有计数器之和
                count =count + entry.getValue();
            }
        }
        return count;
    }

🥝漏桶算法

特点:以固定的速率处理请求(漏水),当请求桶满了后,拒绝请求。

举例:每秒处理 10 个请求,桶的容量是 10,每 0.1 秒固定处理⼀次请求,如果 1 秒 内来了 10 个请求,这 10 此请求都可以处理完。

限流是什么?怎么限流?_服务器_02

但如果 1 秒内来了 11 个请求,最后那个请求就会溢出桶,被拒绝请求。

限流是什么?怎么限流?_redis_03

优点:能够⼀定程度上应对流量突刺,能够固定速率处理请求,保证服务器的安全。

缺点:没有固定速率处理⼀批请求,只能⼀个⼀个按顺序来处理(固定速率的缺点)

/**
     * 每秒处理数(出水率)
     */
    private long rate;

    /**
     *  当前剩余水量
     */
    private long currentWater;

    /**
     * 最后刷新时间
     */
    private long refreshTime;

    /**
     * 桶容量
     */
    private long capacity;

    /**
     * 漏桶算法
     * @return
     */
    boolean leakybucketLimitTryAcquire() {
        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
        long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
        refreshTime = currentTime; // 刷新时间

        // 当前剩余水量还是小于桶的容量,则请求放行
        if (currentWater < capacity) {
            currentWater++;
            return true;
        }
        
        // 当前剩余水量大于等于桶的容量,限流
        return false;
    }

🍗令牌桶限流

令牌桶算法(Token Bucket Algorithm) 令牌桶算法可以看作⼀个令牌桶,其中令牌以恒定的速率产⽣。当⼀个请求到达时,如果令牌桶中仍然有令牌,则该请求得到处理并从令牌桶中减去⼀个令牌。如果令牌桶中没有令牌,则请求将被拒绝。 在此算法中,令牌代表请求能够被处理的数量,⽽桶则代表着请求被处理的容器。

举例:

管理员先⽣成⼀批令牌,每秒⽣成 10 个令牌;当⽤户要操作前,先去拿到⼀个令牌,有令牌的⼈就有资格执⾏操作、同时执⾏操作;拿不到令牌的就等着。

优点:能够并发处理同时的请求,并发性能会更高

令牌桶算法可以缓解漏桶算法的缺点,但在⼀些场景下可能存在⼀定问题。⽐如在 应对短时间内的⾼并发请求时,由于令牌数有限,引⼊过⼤的并发请求会导致严重 的性能问题,也可能会造成请求失败或者拒绝。

/**
     * 每秒处理数(放入令牌数量)
     */
    private long putTokenRate;
    
    /**
     * 最后刷新时间
     */
    private long refreshTime;

    /**
     * 令牌桶容量
     */
    private long capacity;
    
    /**
     * 当前桶内令牌数
     */
    private long currentToken = 0L;

    /**
     * 漏桶算法
     * @return
     */
    boolean tokenBucketTryAcquire() {

        long currentTime = System.currentTimeMillis();  //获取系统当前时间
        long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
        currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
        refreshTime = currentTime; // 刷新时间
        
        //桶里面还有令牌,请求正常处理
        if (currentToken > 0) {
            currentToken--; //令牌数量-1
            return true;
        }
        
        return false;
    }

3.限流粒度

限流粒度是指在进行流量限制时所采用的控制粒度

  1. 针对某个方法限流:这种限流粒度是指对特定的方法进行流量限制,即单位时间内最多允许同时执行X个操作使用这个方法。这可以用于保护某个关键方法不被过度调用,以防止系统过载或资源耗尽。例如,一个系统中可能存在一个查询数据库的方法,为了保护数据库不被大量并发请求压垮,可以对这个方法进行限流,确保每个时刻最多只有X个操作在使用该方法。
  2. 针对某个用户限流:这种限流粒度是针对单个用户的操作进行流量限制,即单个用户在单位时间内最多执行X次操作。这可以用于防止某些用户过度使用系统资源或滥用API接口。例如,在一个Web应用程序中,某个用户可能频繁发送请求,导致服务器负载过高,通过针对该用户进行限流,可以限制其请求的频率,保护系统的稳定性。
  3. 针对某个用户X方法限流:这种限流粒度结合了前两种粒度,针对单个用户在单位时间内对特定方法的调用进行限制。例如,单个用户在单位时间内最多执行X次某个方法。这可以用于在保护关键方法的同时,也防止某些用户对该方法的滥用。这种限流粒度更加精细,可以根据具体情况对特定用户和方法进行细粒度的控制。

通过合理设置限流粒度,可以更好地保护系统的稳定性和性能,防止由于流量过大而导致的系统崩溃或性能下降。同时,也可以防止某些用户或恶意行为对系统造成不必要的负担或损害。

4.限流实现

1.本地限流(单机限流)

每个服务器单独限流,⼀般适⽤于单体项⽬,就是你的项⽬只有⼀个服务器。 在 Java 中,有很多第三⽅库可以⽤来实现单机限流: Guava RateLimiter:这是⾕歌 Guava 库提供的限流⼯具,可以对单位时间内的请求 数量进⾏限制。

import
com.google.common.util.concurrent.RateLimiter;
public static void main(String[] args) {
// 每秒限流5个请求
RateLimiter limiter = RateLimiter.create(5.0);
while (true) {
if (limiter.tryAcquire()) {
// 处理请求
} else {
// 超过流量限制,需要做何处理
}
}
}

2.分布式限流 (集群限流)

分布式限流(集群限流)适用于以下场景:

  1. 高并发系统:在开发高并发系统时,为了保护系统并提升性能,常常需要采用限流措施。分布式限流可以在多个应用服务器之间进行协调,限制整个集群的请求流量,从而保护系统免受过载和崩溃的风险。
  2. 稀缺资源场景:对于稀缺资源的场景,如秒杀、抢购等,需要限制并发请求量,以避免资源被过度消耗或滥用。分布式限流可以在整个集群范围内对这些请求进行限制,确保系统的稳定性和公平性。
  3. 写服务场景:对于写服务场景,如评论、下单等,需要限制请求的速率,以减轻数据库和服务器的压力。分布式限流可以根据集群的负载情况和容量限制,对写服务进行限速,防止系统过载。
  4. 频繁复杂查询场景:对于频繁复杂查询的场景,如评论的最后几页等,需要限制查询的请求量,避免对数据库产生过大的负载。分布式限流可以限制这类查询的请求速率,保护数据库的稳定性和性能。

总之,分布式限流适用于需要在多个应用服务器之间进行协调限流的场景,可以保护系统免受过载和崩溃的风险,并确保资源的合理使用和公平性。在选择是否使用分布式限流时,需要综合考虑系统的需求、特点以及集群的规模等因素。

3.Redisson 限流实现

Redisson 内置了⼀个限流工具类,可以帮助你利⽤ Redis 来存储、来统计。

①Redis安装包

链接:https://pan.baidu.com/s/1JFVJ4rLo3yYQAHdClYuuAg?pwd=togj 
提取码:togj 

限流是什么?怎么限流?_java_04

 启动成功:

限流是什么?怎么限流?_java_05

②引⼊ Redisson 依赖

<dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.21.3</version>
        </dependency>

③application.yml

redis:
    database: 1
    host: localhost
    port: 6379
    timeout: 5000

 

④创建 RedissonConfig 配置类,⽤于初始化 RedissonClient 对象单例:

@Data
@ConfigurationProperties(prefix = "spring.redis")
@Configuration
public class RedissonConfig {
    private Integer database;
    private String host;
    private Integer port;
    private String password;
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setDatabase(database)
                .setAddress("redis://" + host + ":" + port)
                .setPassword(password);
        RedissonClient redisson = Redisson.create();
        return redisson;
    }
}

⑤测试

@SpringBootTest
class RedisLimiterTest {

    @Resource
    private RedissonClient redissonClient;

    public  void doRateLimit(String key) {
    // 创建⼀个限流器
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
    // 每秒最多访问 2 次
    // 参数1 type:限流类型,可以是⾃定义的任何类型,⽤于区分不同的限流策
    // 参数2 rate:限流速率,即单位时间内允许通过的请求数量。
    // 参数3 rateInterval:限流时间间隔,即限流速率的计算周期⻓度。
    // 参数4 unit:限流时间间隔单位,可以是秒、毫秒等。
        rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
    // 每当⼀个操作来了后,请求⼀个令牌
        boolean canOp = rateLimiter.tryAcquire(1);
       if (!canOp){
           throw  new RuntimeException("请求过于频繁");
       }
    }


    @Test
    void doRateLimit() throws InterruptedException {
    // 模拟⼀下操作
        String userId = "1";
    // 瞬间执⾏2次,每成功⼀次,就打印'成功'
        for (int i = 0; i < 3; i++) {
            doRateLimit(userId);
            System.out.println("成功");
        }

    }
}

因为限流器设置为 1秒能接受2个请求。但是测试中  1秒发送了3个请求,故此在第三次请求的时候,会抛出异常。

限流是什么?怎么限流?_服务器_06

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年12月23日 0

暂无评论

推荐阅读
n3y4rZ8GUfmO