JAVA应用层限流

限流就是对请求或并发数进行限制;通过对一个时间窗口内的请求量进行限制来保障系统的正常运行。如果我们的服务资源有限、处理能力有限,就需要对调用我们服务的上游请求进行限制,以防止自身服务由于资源耗尽而停止服务。
如:公交车满载拒载、地铁站限流排队等

限流中提到的阈值拒绝策略两个概念
阈值:在一个单位时间内允许的请求量。如 QPS 限制为10,说明 1 秒内最多接受 10 次请求。
拒绝策略:超过阈值的请求的拒绝策略,常见的拒绝策略有直接拒绝、排队等待等。

1. 计数器算法

一种简单方便的限流算法。通过一个支持原子操作的计数器来累计 1 秒内的请求次数,当 1 秒内计数达到限流阈值时触发拒绝策略。每过 1 秒,计数器重置为 0 开始重新计数。

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 计数限流器
 */
public class CountLimiter {

    // 阈值
    private Integer qps;
    // 时间窗口(毫秒)
    private long timeWindows = 1000;
    // 计数器
    private AtomicInteger requestCount = new AtomicInteger();

    private long startTime = System.currentTimeMillis();

    private CountLimiter(){}

    public CountLimiter(Integer qps) {
        this.qps = qps;
    }

    public synchronized boolean tryAcquire() {
        if ((System.currentTimeMillis() - startTime) > timeWindows) {
            requestCount.set(0);
            startTime = System.currentTimeMillis();
        }
        return requestCount.incrementAndGet() <= qps;
    }

}

测试

public class RateLimiterTest {

    public static void main(String[] args) throws InterruptedException {
        CountLimiter countLimiter = new CountLimiter(2);
        for (int i = 0; i < 10; i++) {
            Thread.sleep(200);
            LocalTime now = LocalTime.now();
            if (!countLimiter.tryAcquire()) {
                System.out.println(now + ": 限流 ");
            } else {
                System.out.println(now + ": 通行 ");
            }
        }
    }
}

2. 滑动计数器算法

滑动计数器算法为解决计数器算法遇到时间窗口的临界突变时,会发生qps突增,如 1s 中的后 500 ms 和第 2s 的前 500ms 时,虽然是加起来是 1s 时间,却可以被请求 2 次以上。

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 滑动计数器限流
 */
public class SlidingCountLimiter  {

    /**
     * 阈值
     */
    private int qps = 2;

    /**
     * 时间窗口大小(毫秒)
     */
    private long windowSize = 1000;

    /**
     * 子窗口数量
     */
    private Integer windowCount = 10;

    /**
     * 窗口列表
     */
    private WindowInfo[] windowArray = new WindowInfo[windowCount];

    private SlidingCountLimiter() {}

    public SlidingCountLimiter(int qps) {
        this.qps = qps;
        long currentTimeMillis = System.currentTimeMillis();
        for (int i = 0; i < windowArray.length; i++) {
            windowArray[i] = new WindowInfo(currentTimeMillis, new AtomicInteger(0));
        }
    }

    /**
     * 获取权限
     * @return
     */
    public synchronized boolean tryAcquire() {
        long currentTimeMillis = System.currentTimeMillis();
        // 当前时间窗口 = 当前时间 % 时间窗口大小 / (时间窗口大小 / 子窗口数量)
        int currentIndex = (int)(currentTimeMillis % windowSize / (windowSize / windowCount));
        // 更新当前窗口计数 & 重置过期窗口计数
        int sum = 0;
        for (int i = 0; i < windowArray.length; i++) {
            WindowInfo windowInfo = windowArray[i];
            if ((currentTimeMillis - windowInfo.getTime()) > windowSize) {
                windowInfo.getNumber().set(0);
                windowInfo.setTime(currentTimeMillis);
            }
            if (currentIndex == i && windowInfo.getNumber().get() < qps) {
                windowInfo.getNumber().incrementAndGet();
            }
            sum = sum + windowInfo.getNumber().get();
        }
        // 当前 QPS 是否超过限制
        return sum <= qps;
    }

    private class WindowInfo {
        // 窗口开始时间
        private Long time;
        // 计数器
        private AtomicInteger number;

        public WindowInfo(long time, AtomicInteger number) {
            this.time = time;
            this.number = number;
        }

        public Long getTime() {
            return time;
        }

        public void setTime(Long time) {
            this.time = time;
        }

        public AtomicInteger getNumber() {
            return number;
        }

        public void setNumber(AtomicInteger number) {
            this.number = number;
        }
    }

}

测试

public class RateLimiterTest {

    public static void main(String[] args) throws InterruptedException {
		SlidingCountLimiter slidingCountLimiter = new SlidingCountLimiter(2);
        for (int i = 0; i < 20; i++) {
            Thread.sleep(200);
            if (slidingCountLimiter.tryAcquire()) {
                System.out.print(LocalTime.now() + ": 通行 ");
            } else {
                System.out.println(LocalTime.now() + ": 限流 ");
            }
        }
    }
    
}

3. 令牌桶算法 (推荐)

每个请求都需要去桶中拿取一个令牌,取到令牌则继续执行;如果桶中无令牌可取,就触发拒绝策略,可以是超时等待,也可以是直接拒绝本次请求,由此达到限流目的。

使用Google 的 Java 开发工具包 Guava 中的限流工具类 RateLimiter

<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>28.2-jre</version>
</dependency>
public class RateLimiterTest {

    public static void main(String[] args) throws InterruptedException {
		RateLimiter rateLimiter = RateLimiter.create(2);
        for (int i = 0; i < 10; i++) {
            String time = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME);
            System.out.println(time + ":" + rateLimiter.tryAcquire());
            Thread.sleep(200);
        }
    }
    
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值