SpringBoot+Redis实战⾼并发限流,这7种姿势有点厉害前⾔
最近⼏年,随着微服务的流⾏,服务和服务之间的依赖越来越强,调⽤关系越来越复杂,服务和服务之间的稳定性越来越重要。在遇到突发的请求量激增,恶意的⽤户访问,亦或请求频率过⾼给下游服务带来较⼤压⼒时,我们常常需要通过缓存、限流、熔断降级、负载均衡等多种⽅式保证服务的稳定性。其中限流是不可或缺的⼀环,这篇⽂章介绍限流相关知识。
1. 限流
限流顾名思义,就是对请求或并发数进⾏限制;通过对⼀个时间窗⼝内的请求量进⾏限制来保障系统的正常运⾏。如果我们的服务资源有限、处理能⼒有限,就需要对调⽤我们服务的上游请求进⾏限制,以防⽌⾃⾝服务由于资源耗尽⽽停⽌服务。
在限流中有两个概念需要了解。
阈值:在⼀个单位时间内允许的请求量。如 QPS 限制为10,说明 1 秒内最多接受 10 次请求。
拒绝策略:超过阈值的请求的拒绝策略,常见的拒绝策略有直接拒绝、排队等待等。
2. 固定窗⼝算法
固定窗⼝算法⼜叫计数器算法,是⼀种简单⽅便的限流算法。主要通过⼀个⽀持原⼦操作的计数器来累计 1 秒内的请求次数,当 1 秒内计数达到限流阈值时触发拒绝策略。每过 1 秒,计数器重置为 0 开始重新计数。
2.1. 代码实现
下⾯是简单的代码实现,QPS 限制为 2,这⾥的代码做了⼀些优化,并没有单独开⼀个线程去每隔 1 秒重置计数器,⽽是在每次调⽤时进⾏时间间隔计算来确定是否先重置计数器。
1/**
2 * @author www.wdbyte
3 */
4public class RateLimiterSimpleWindow {
5    // 阈值
6    private static Integer QPS = 2;
7    // 时间窗⼝(毫秒)
8    private static long TIME_WINDOWS = 1000;
9    // 计数器
10    private static AtomicInteger REQ_COUNT = new AtomicInteger();
11
12    private static long START_TIME = System.currentTimeMillis();
13
14    public synchronized static boolean tryAcquire() {
15        if ((System.currentTimeMillis() - START_TIME) > TIME_WINDOWS) {
16            REQ_COUNT.set(0);
17            START_TIME = System.currentTimeMillis();
18        }
19        return REQ_COUNT.incrementAndGet() <= QPS;
20    }
21
22    public static void main(String[] args) throws InterruptedException {
23        for (int i = 0; i < 10; i++) {
24            Thread.sleep(250);
25            LocalTime now = w();
26            if (!tryAcquire()) {
27                System.out.println(now + " 被限流");
28            } else {
29                System.out.println(now + " 做点什么");
30            }
31        }
32    }
33}
运⾏结果:
120:53:43.038922 做点什么
220:53:43.291435 做点什么
320:53:43.543087 被限流
420:53:43.796666 做点什么
520:53:44.050855 做点什么
620:53:44.303547 被限流
720:53:44.555008 被限流
820:53:44.809083 做点什么
920:53:45.063828 做点什么
1020:53:45.314433 被限流
从输出结果中可以看到⼤概每秒操作 3 次,由于限制 QPS 为 2,所以平均会有⼀次被限流。看起来可以了,不过我们思考⼀下就会发现这种简单的限流⽅式是有问题的,虽然我们限制了 QPS 为 2,但是当遇到时间窗⼝的临界突变时,如 1s 中的后 500 ms 和第 2s 的前
500ms 时,虽然是加起来是 1s 时间,却可以被请求 4 次。
固定窗⼝算法
简单修改测试代码,可以进⾏验证:
得到输出中可以看到连续 4 次请求,间隔 250 ms 没有却被限制。:
3. 滑动窗⼝算法
我们已经知道固定窗⼝算法的实现⽅式以及它所存在的问题,⽽滑动窗⼝算法是对固定窗⼝算法的改进。既然固定窗⼝算法在遇到时间窗⼝的临界突变时会有问题,那么我们在遇到下⼀个时间窗⼝前也调整时间窗⼝不就可以了吗?
下⾯是滑动窗⼝的⽰意图。
1
// 先休眠 400ms ,可以更快的到达时间窗⼝。2
Thread.sleep(400);3
for (int i = 0; i < 10; i++) {4
Thread.sleep(250);5
if (!tryAcquire()) {6
System.out.println("被限流");7
} else {8
System.out.println("做点什么");9    }10}
1
20:51:17.395087 做点什么2
20:51:17.653114 做点什么3
20:51:17.903543 做点什么4
20:51:18.154104 被限流5
20:51:18.405497 做点什么6
20:51:18.655885 做点什么7
20:51:18.906177 做点什么8
20:51:19.158113 被限流9
20:51:19.410512 做点什么1020:51:19.661629 做点什么
滑动窗⼝算法
springboot推荐算法上图的⽰例中,每 500ms 滑动⼀次窗⼝,可以发现窗⼝滑动的间隔越短,时间窗⼝的临界突变问题发⽣的概率也就越⼩,不过只要有时间窗⼝的存在,还是有可能发⽣时间窗⼝的临界突变问题。
3.1. 代码实现
下⾯是基于以上滑动窗⼝思路实现的简单的滑动窗⼝限流⼯具类。
1package com.wdbyte.rate.limiter;
2
3import java.time.LocalTime;
4import urrent.atomic.AtomicInteger;
5
6/**
7 * 滑动窗⼝限流⼯具类
8 *
9 * @author www.wdbyte
10 */
11public class RateLimiterSlidingWindow {
12    /**
13    * 阈值
14    */
15    private int qps = 2;
16    /**
17    * 时间窗⼝总⼤⼩(毫秒)
18    */
19    private long windowSize = 1000;
20    /**
21    * 多少个⼦窗⼝
22    */
23    private Integer windowCount = 10;
24    /**
25    * 窗⼝列表
26    */
27    private WindowInfo[] windowArray = new WindowInfo[windowCount];
28
29    public RateLimiterSlidingWindow(int qps) {
30        this.qps = qps;
31        long currentTimeMillis = System.currentTimeMillis();
32        for (int i = 0; i < windowArray.length; i++) {
33            windowArray[i] = new WindowInfo(currentTimeMillis, new AtomicInteger(0));
34        }
35    }
36
37    /**
38    * 1. 计算当前时间窗⼝
39    * 2. 更新当前窗⼝计数 & 重置过期窗⼝计数
40    * 3. 当前 QPS 是否超过限制
41    *
42    * @return
43    */
44    public synchronized boolean tryAcquire() {
45        long currentTimeMillis = System.currentTimeMillis();
46        // 1. 计算当前时间窗⼝
47        int currentIndex = (int)(currentTimeMillis % windowSize / (windowSize / windowCount));
48        // 2.  更新当前窗⼝计数 & 重置过期窗⼝计数
49        int sum = 0;
50        for (int i = 0; i < windowArray.length; i++) {
51            WindowInfo windowInfo = windowArray[i];
52            if ((currentTimeMillis - Time()) > windowSize) {
53                Number().set(0);
54                windowInfo.setTime(currentTimeMillis);
55            }
56            if (currentIndex == i && Number().get() < qps) {
56            if (currentIndex == i && Number().get() < qps) {
57                Number().incrementAndGet();
58            }
59            sum = sum + Number().get();
60        }
61        // 3. 当前 QPS 是否超过限制
62        return sum <= qps;
63    }
64
65    private class WindowInfo {
66        // 窗⼝开始时间
67        private Long time;
68        // 计数器
69        private AtomicInteger number;
70
71        public WindowInfo(long time, AtomicInteger number) {
72            this.time = time;
73            this.number = number;
74        }
75        //
76    }
77}
下⾯是测试⽤例,设置 QPS 为 2,测试次数 20 次,每次间隔 300 毫秒,预计成功次数在 12 次左右。
1public static void main(String[] args) throws InterruptedException {
2    int qps = 2, count = 20, sleep = 300, success = count * sleep / 1000 * qps;
3    System.out.println(String.format("当前QPS限制为:%d,当前测试次数:%d,间隔:%dms,预计成功次数:%d", qps, count, sleep, success));
4    success = 0;
5    RateLimiterSlidingWindow myRateLimiter = new RateLimiterSlidingWindow(qps);
6    for (int i = 0; i < count; i++) {
7        Thread.sleep(sleep);
8        if (Acquire()) {
9            success++;
10            if (success % qps == 0) {
11                System.out.w() + ": success, ");
12            } else {
13                System.out.w() + ": success, ");
14            }
15        } else {
16            System.out.w() + ": fail");
17        }
18    }
19    System.out.println();
20    System.out.println("实际测试成功次数:" + success);
21}
下⾯是测试的结果。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。