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小时内删除。
发表评论