常见限流算法

限流算法

常见的限流算法有四种:

  1. 计数器算法
  2. 滑动窗口算法
  3. 漏桶算法
  4. 令牌桶算法

实际应用上,计数器算法很少使用,其他三种根据对突发请求的处理可大体上分为两类:

  • 滑动窗口、令牌桶
    • 只要没有超过限制(窗口内请求数未达上限、存在令牌)就进行接收
  • 漏桶
    • 溢出的突发请求会被拒绝

计数器算法

又被称为固定窗口算法

  • 从第一次接收到请求开始计数,维护一个有效期为一分钟的计数器
  • 后续每接收到一个请求计数器加一
  • 当到达最大请求数时,拒绝执行后续请求
  • 计数器过期后,重复第一步

实现

import java.util.concurrent.atomic.AtomicInteger;

public class CounterRateLimiter {
    private static final int LIMIT = 200; // 每秒最多允许的请求数量
    private static final int PERIOD = 60 * 1000; // 时间周期,以毫秒为单位,例如这里设置为60秒
    private AtomicInteger counter = new AtomicInteger(0); // 请求计数器
    private long startTime = System.currentTimeMillis(); // 当前周期开始的时间

    /**
     * 尝试获取访问令牌
     * @return 如果允许请求则返回true,否则返回false
     */
    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        if (currentTime - startTime > PERIOD) {
            // 重置计数器
            startTime = currentTime;
            counter.set(0);
        }
        return counter.incrementAndGet() <= LIMIT;
    }
}

缺点

以下情景假设:

  • 服务端每分钟最多处理 200 个请求

临界问题

某一时刻出现突发请求,窗口剩余时间内,服务处于不可用状态

滑动窗口算法

用于解决计数器算法的临界问题

  • 根据计数周期进行均分,将其划分为若干块 (假设为n个)
  • 设定一个窗口,窗口包含n个块
  • 窗口随着随着时间进行滑动,丢掉最先出现的块,加入最新的块
  • 算法保证:一个窗口内的请求次数不会超过限定值

实现

根据窗口的特点:

  • 块:先进先出 --> 队列
  • 频繁在队首插入,队尾移除 --> 链表

数据结构:

  • 双向链表表示窗口
  • 链表内存储原子整数表示当前块的请求次数
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class SlidingWindowRateLimiter {
    private static final int LIMIT = 100; // 每秒最多允许的请求数量
    private static final int PERIOD = 60 * 1000; // 时间周期,以毫秒为单位,例如这里设置为1分钟
    private Queue<AtomicLong> windowList = new LinkedList<>(); // 存储每个时间窗口的请求计数
    private AtomicLong currentWindowRequests = new AtomicLong(0);

    public SlidingWindowRateLimiter() {
        windowList.add(new AtomicLong(0));
    }

    /**
     * 尝试获取访问令牌
     * @return 如果允许请求则返回true,否则返回false
     */
    public synchronized boolean tryAcquire() {
        long currentTime = System.currentTimeMillis();
        long currentWindow = currentTime / PERIOD * PERIOD;

        // 更新当前时间窗口
        if (currentWindow != currentWindowRequests.get()) {
            // 移除过期窗口数据
            while (!windowList.isEmpty() && windowList.peek().get() < currentWindow - PERIOD) {
                windowList.poll();
            }
            // 添加新窗口
            if (windowList.peekLast() == null || windowList.peekLast().get() != currentWindow) {
                windowList.add(new AtomicLong(0));
            }
            // 设置当前窗口
            currentWindowRequests.set(currentWindow);
        }

        // 获取当前窗口的计数器
        AtomicLong counter = windowList.peekLast();
        // 增加请求计数
        if (counter.incrementAndGet() > LIMIT) {
            // 超过限制,回退计数
            counter.decrementAndGet();
            return false;
        }
        return true;
    }
}

优缺

滑动窗口在面对突发请求时,可以将服务不可用的时间减少至一个区间

  • 区间块数划分越多,效果越明显
  • 但是区间块数越多,服务端需要保存数据也越多,耗费内存

需要注意的是

  • 滑动窗口无法处理临界问题
  • 滑动窗口只能缓解突发请求的问题,划分的块越多,不可用的时间就越少

以下情景假设:

  • 服务端每分钟最多处理 200 个请求

漏桶算法

类似于消息队列MQ削峰,可以处理临界问题

  • 区别在于:MQ通常不会轻易丢弃消息,漏桶很可能会丢弃请求

服务器引入一个队列存储到达的请求,并按照一定规则从队列取出和处理请求

  • 一定规则:通常匀速处理请求
    • 假设每分钟最多300次请求,则每秒处理300 / 60 = 5个请求
  • 拒绝请求的时机:队列已满

实现

漏桶算法的作用和机理与MQ十分类似

  • 可以固定MQ的队列长度,使用MQ来实现漏桶算法

优缺

服务器通常按照恒定速率处理请求,无法很好的发挥最大效率

令牌桶算法

用于解决上述算法的问题:

  • 临界问题,请求数会超过限制
    • 保证请求周期内生成令牌数不超过限制
  • 遇到突发请求,会出现长时间服务不可用的问题
    • 只要能获取到令牌,服务器是全速进行处理的

漏桶算法:

几个抽象的角色:

  • 加工厂:
    • 生产令牌
  • 仓库:
    • 存储令牌
  • 消费者:
    • 加工厂员工使用令牌接待消费者

服务器类似加工厂,按照一定规则生成令牌并存储

  • 一定规则:
    • 通常是根据限流规则均匀生成
    • 假设每分钟最多300次请求,则每秒生成300 / 60 = 5个令牌,之后存储到仓库
      • 仓库已满时:丢弃溢出的令牌

请求类似消费者,服务器处理请求时需要消耗一个令牌

实现

令牌桶实际应用比较多,可以直接使用第三方提供的组件

  • 单体架构:Guava
  • 分布式架构:Redisson

Guava

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>33.2.1-jre</version>
</dependency>
import com.google.common.util.concurrent.RateLimiter;

// 每秒放入 5 个令牌
RateLimiter rateLimiter = RateLimiter.create(5.0);
@Test
public void test() throws IOException {

    // 阻塞获取令牌 默认获取 1个
    // rateLimiter.acquire();
    
    // 执行业务逻辑...
    
    // 尝试获取令牌 默认获取 1个
    // 还可设置等待时长、获取令牌个数
    boolean acquire = rateLimiter.tryAcquire();
    
    if (acquire) {
        // 执行业务逻辑...
    
    }

}

Redisson

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.26.0</version>
</dependency>
@Bean
RedissonClient redissonClient(RedisProperties redisProperties) {
    Config config = new Config();
    
    SingleServerConfig singleServerConfig = config.useSingleServer();
    singleServerConfig.setAddress(String.format("redis://%s:%s", "127.0.0.1", 7890));
    singleServerConfig.setDatabase(0);
    singleServerConfig.setPassword("pwd");
    
    return Redisson.create(config);
}
@Resource
private RedissonClient redissonClient;

@Test
public void test() throws IOException {

    String limitKey = "limit_request_count_biz1";
    RRateLimiter rateLimiter = redissonClient.getRateLimiter(limitKey);
    
    // 每秒产生 5 个令牌
    rateLimiter.trySetRate(RateType.OVERALL, 5, 1, RateIntervalUnit.SECONDS);
    
    // 不断自旋尝试获取锁
    while (rateLimiter.tryAcquire());
    
    // 执行业务逻辑
}

优缺

  • 与漏桶算法
    • 可以应对突发请求,更快的进行处理
  • 与滑动窗口
    • 功能类似,大量的突发请求会影响其他正常的请求
      • 令牌机制解偶了请求验证和处理,或许根据令牌可以更方便地进行请求处理前地验证

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值