API限流设计

  • 记于:2025-11-24 晚上
  • 地点:浙江省·温州市·家里
  • 天气:晴天

背景#

个人开源项目[yeah-boot]需要一个流控/限流组件;
虽然有一些成熟的开源流控组件,比如Alibaba Sentinel、Bucket4j、Guava RateLimiter等;
但就是想自己造个轮子。

功能#

“限流”与“流控”的关联与区别(by AI):

1
2
3
限流(Rate Limit)是控制单位时间内请求数量的策略,核心是限制流量,通常超限就拒绝请求。
流控(Flow Control)是更广义的概念,除了限流,还可以排队、削峰、降级或动态调节,目标是保证系统整体稳定。
一句话总结:限流是流控的一部分,流控是系统稳定性保护的综合手段。

我做的偏向于“限流”,但是模块叫“流控”,方便日后朝“流控”方向扩充功能;

主要功能:

  • 支持“每N秒最多M个请求”
  • 支持“每个分组内每N秒最多支持M个请求”
  • 支持“每N秒内最多M个分组数量”
  • 支持按IP分组,也支持自定义分组(支持spel)

设计思路#

核心思路:基于时间窗口内的请求记录与统计做控制。

主要配置参数#

1
2
3
4
5
6
7
8
9
10
11
12
boolean enabled() default true; // 是否启用
String name() default ""; // 名称,默认为方法的全限定名
GroupType groupType() default GroupType.NONE; // 分组方式:1-无 2-按IP 3-按用户
String customGroup() default ""; // 自定义分组逻辑,配合GroupType.CUSTOM使用;支持spel表达式
int limitCount() default -1; // 限制数量
int limitGroup() default -1; // 限制分组数量
int limitGroupCount() default -1; // 限制每个分组内数量
int timeWindow() default 1000; // 时间窗口,单位:毫秒
int bucketSize() default 100; // 时间窗口内桶大小,单位:毫秒,默认:100
boolean dynamicTimeWindow() default true; // 是否动态时间窗口模式
boolean global() default false; // 是否全局限流 TODO 后续支持
String description() default ""; // 描述

使用示例#

1
2
3
4
5
6
@PublicAccess
@RateLimit(timeWindow = 2000, limitCount = 5, description = "限制2秒内最多5个请求")
@GetMapping("/test")
public String test() {
return "hello world";
}

非分组方式的限流#

主要参数:limitCount, timeWindow, bucketSize

大概思路:
使用滑动时间窗口,将timeWindow窗口大小按照bucketSize大小每个桶进行均分;
将窗口做成动态一直向前滚动的轮状数据结构(需要进行滚动(滑动)操作);
每个请求到来时,先重置过期桶的计数,然后将当前请求落入到对应的桶中;
计算当前所有桶的计数,判断是否超限。
个人感觉轮状机制+过期重置的逻辑有些难理解,有点很细致的东西在里面,具体见下方代码。

分组方式的限流#

主要参数:limitGroupCount, timeWindow, bucketSize, groupType, customGroup

大概思路:
大概的思路与【非分组方式的限流】一致;
只不过多个一个分组维度。
具体见下方代码。

针对分组数量的限流控制#

主要参数:limitGroup, timeWindow

大概思路:
时间窗口内,记录每个分组的最新一次请求时间,计算窗口内分组数,判断是否超限。

其他#

在代码实现上要尽量保证准确性,需要在一些操作上加锁,也要兼顾性能,锁粒度不能太大;
之前的一个版本,是将请求的时间都记录下来,基于此做统计和判断;
但是数据量大的时候会有存储和性能问题,而且没加锁,有并发下的准确性问题;
后来询问AI做了参考,使用了常用的以计数方式(AtomicInteger)为主,同时进行了加锁。
具体见下方代码。

代码实现#

配置参数类#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.yeshimin.yeahboot.flowcontrol.ratelimit;

import com.yeshimin.yeahboot.flowcontrol.enums.GroupType;

import java.lang.annotation.*;

/**
* 限流注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

/**
* 是否启用
*/
boolean enabled() default true;

/**
* 名称,默认为方法的全限定名
*/
String name() default "";

/**
* 分组方式:1-无 2-按IP 3-按用户
*/
GroupType groupType() default GroupType.NONE;

/**
* 自定义分组逻辑,配合GroupType.CUSTOM使用;支持spel表达式
*/
String customGroup() default "";

/**
* 限制数量
*/
int limitCount() default -1;

/**
* 限制分组数量
*/
int limitGroup() default -1;

/**
* 限制每个分组内数量
*/
int limitGroupCount() default -1;

/**
* 时间窗口,单位:毫秒
*/
int timeWindow() default 1000;

/**
* 时间窗口内桶大小,单位:毫秒,默认:100
*/
int bucketSize() default 100;

/**
* 是否动态时间窗口模式
*/
boolean dynamicTimeWindow() default true;

/**
* 是否全局限流 TODO
*/
boolean global() default false;

/**
* 描述
*/
String description() default "";
}

主要逻辑代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
package com.yeshimin.yeahboot.flowcontrol.ratelimit;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
* RateLimit 拦截器
*/
@Slf4j
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

private static final ExpressionParser PARSER = new SpelExpressionParser();
private static final StandardEvaluationContext CONTEXT = new StandardEvaluationContext();

// map<res, window>
private final Map<String, SlidingWindow> plainHolder = new ConcurrentHashMap<>();
// map<res, map<group, latestReqTime>>
private final Map<String, ConcurrentHashMap<String, Long>> outerGroupHolder = new ConcurrentHashMap<>();
// map<res, map<group, window>>
private final Map<String, ConcurrentHashMap<String, SlidingWindow>> innerGroupHolder = new ConcurrentHashMap<>();

// 每5分钟执行一次清理任务
private static final long CLEAN_INTERVAL_MS = 5 * 60 * 1000;
// 10分钟无请求则清理
private static final long GROUP_EXPIRE_MS = 10 * 60 * 1000;

@Autowired(required = false)
private RateLimitService rateLimitService;

@PostConstruct
public void init() {
log.debug("init [yeah-boot] rate limit interceptor...");
}

private static final ScheduledExecutorService CLEANER =
Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "RateLimitCleaner");
t.setDaemon(true);
return t;
});

// constructor
public RateLimitInterceptor() {
CLEANER.scheduleAtFixedRate(this::cleanExpiredGroups,
CLEAN_INTERVAL_MS, CLEAN_INTERVAL_MS, TimeUnit.MILLISECONDS);
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
log.debug("Not a method handler, skip");
return true;
}

HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);

// 没有注解或禁用限流
if (rateLimit == null || !rateLimit.enabled()) {
log.debug("No rate limit annotation or disabled, skip");
return true;
}

// --------------------------------------------------------------------------------

// 获取限流配置
RateLimitConf rlConf = this.getRateLimitConf(rateLimit, method, request);
log.debug("rlConf: {}", rlConf);
if (rlConf.isSkip()) {
log.debug("Skip rate limit, skip");
return true;
}

// --------------------------------------------------------------------------------

// check limitCount
SlidingWindow plainWindow = null;
if (rlConf.getLimitCount() > -1) {
log.debug("check for [limitCount]");

plainWindow = plainHolder.computeIfAbsent(rlConf.getResName(), k -> new SlidingWindow(rlConf, "[limitCount]"));
log.debug("plainWindow: {}", plainWindow);

boolean allowed = plainWindow.tryAcquire(rlConf.getLimitCount(), rlConf.getReqTime());
if (!allowed) {
log.debug("Rate limit exceeded: limitCount");
response.setStatus(429);
response.getWriter().write("Rate limit exceeded: limitCount");
return false;
}
}

// check limitGroup
ConcurrentHashMap<String, Long> outerGroupMap = null;
if (rlConf.getLimitGroup() > -1) {
log.debug("check for [limitGroup]");
outerGroupMap = outerGroupHolder.computeIfAbsent(rlConf.getResName(), k -> new ConcurrentHashMap<>());

// 删除过期的分组
outerGroupMap.entrySet().removeIf(entry -> {
long reqTime = entry.getValue();
if (reqTime < rlConf.getWindowFrom()) {
log.debug("Remove expired group:{}, windowFrom: {}, reqTime: {}", entry.getKey(), rlConf.getWindowFrom(), reqTime);
return true;
}
return false;
});

// 判断是否超限
boolean allowed;
if (outerGroupMap.containsKey(rlConf.getGroupName())) {
allowed = outerGroupMap.size() <= rlConf.getLimitGroup();
} else {
allowed = outerGroupMap.size() + 1 <= rlConf.getLimitGroup();
}

if (!allowed) {
log.debug("Rate limit exceeded: limitGroup");
response.setStatus(429);
response.getWriter().write("Rate limit exceeded: limitGroup");
return false;
}
}

// check limitGroupCount
SlidingWindow innerGroupWindow = null;
if (rlConf.getLimitGroupCount() > -1) {
log.debug("check for [limitGroupCount]");

ConcurrentHashMap<String, SlidingWindow> map = innerGroupHolder.computeIfAbsent(rlConf.getResName(), k -> new ConcurrentHashMap<>());
innerGroupWindow = map.computeIfAbsent(rlConf.getGroupName(), k -> new SlidingWindow(rlConf, "[limitGroupCount]"));
log.debug("innerGroupWindow: {}", innerGroupWindow);

boolean allowed = innerGroupWindow.tryAcquire(rlConf.getLimitGroupCount(), rlConf.getReqTime());

if (!allowed) {
log.debug("Rate limit exceeded: limitGroupCount");
response.setStatus(429);
response.getWriter().write("Rate limit exceeded: limitGroupCount");
return false;
}
}

// 仅当通过所有限流检查,记录请求
if (rlConf.getLimitCount() > -1) {
log.debug("Record request for [limitCount]");
Objects.requireNonNull(plainWindow).increase(rlConf.getReqTime());
}
if (rlConf.getLimitGroup() > -1) {
log.debug("Record request for [limitGroup]");
Objects.requireNonNull(outerGroupMap).put(rlConf.getGroupName(), rlConf.getReqTime());
}
if (rlConf.getLimitGroupCount() > -1) {
log.debug("Record request for [limitGroupCount]");
Objects.requireNonNull(innerGroupWindow).increase(rlConf.getReqTime());
}

return true;
}

/**
* 获取RateLimitConf配置
*/
private RateLimitConf getRateLimitConf(RateLimit rl, Method m, HttpServletRequest req) {
RateLimitConf conf = new RateLimitConf();
conf.setEnabled(rl.enabled());
conf.setName(rl.name());
conf.setGroupType(rl.groupType());
conf.setCustomGroup(rl.customGroup());
conf.setLimitCount(rl.limitCount());
conf.setLimitGroup(rl.limitGroup());
conf.setLimitGroupCount(rl.limitGroupCount());
conf.setTimeWindow(rl.timeWindow());
conf.setBucketSize(rl.bucketSize());
conf.setDynamicTimeWindow(rl.dynamicTimeWindow());
conf.setGlobal(rl.global());

// 修整参数
if (conf.getLimitCount() < -1) {
conf.setLimitCount(-1);
}
if (conf.getLimitGroup() < -1) {
conf.setLimitGroup(-1);
}
if (conf.getLimitGroupCount() < -1) {
conf.setLimitGroupCount(-1);
}

// 请求时间
conf.setReqTime(System.currentTimeMillis());

// 修整参数(最小时间窗口为1000毫秒)
if (conf.getTimeWindow() < 1000) {
conf.setTimeWindow(1000);
}
// 最小桶大小为100毫秒,且必须能整除时间窗口
if (conf.getBucketSize() < 100) {
conf.setBucketSize(100);
}

// 计算窗口时间范围
this.calcWindowTimeRange(conf);

// 资源名称
conf.setResName(this.getResName(conf, m));
// 分组名称
conf.setGroupName(this.getGroupName(conf, m, req));

return conf;
}

/**
* 计算时间窗口起始时间
* 根据是否动态时间窗口选择计算方式
*/
private void calcWindowTimeRange(RateLimitConf rlConf) {
if (rlConf.isDynamicTimeWindow()) {
rlConf.setWindowFrom(rlConf.getReqTime() - rlConf.getTimeWindow());
rlConf.setWindowTo(rlConf.getReqTime());
} else {
rlConf.setWindowFrom(rlConf.getReqTime() / rlConf.getTimeWindow() * rlConf.getTimeWindow());
rlConf.setWindowTo(rlConf.getWindowFrom() + rlConf.getTimeWindow());
}
}

/**
* 获取目标资源的名称(一般为接口方法的全限定名称或者自定义名称)
*/
private String getResName(RateLimitConf rlConf, Method m) {
return rlConf.getName() == null || rlConf.getName().trim().isEmpty() ?
m.getDeclaringClass().getName() + "." + m.getName() : rlConf.getName();
}

/**
* 滑动窗口实现(环形桶)
*/
private static class SlidingWindow {
private final int windowSizeMs; // 整体窗口大小
private final int bucketSizeMs; // 每个桶的时间长度
private final int bucketCount; // 桶数量
private final AtomicInteger[] buckets; // 每个桶计数
private volatile long lastUpdateTime; // 最后更新时间(毫秒)
private volatile long lastGlobalBucketIndex; // 最后更新全局桶索引

@Getter
@Setter
private volatile long lastReqTime;

private String logFlag;

public SlidingWindow(RateLimitConf rlConf, String logFlag) {
this.windowSizeMs = rlConf.getTimeWindow();
this.bucketSizeMs = rlConf.getBucketSize();
// 最好能整除,否则行为未知
this.bucketCount = this.windowSizeMs / this.bucketSizeMs;
this.buckets = new AtomicInteger[bucketCount];
for (int i = 0; i < bucketCount; i++) {
buckets[i] = new AtomicInteger(0);
}
this.lastUpdateTime = System.currentTimeMillis();
this.lastGlobalBucketIndex = lastUpdateTime / this.bucketSizeMs;

// 上次请求时间
this.lastReqTime = System.currentTimeMillis();

this.logFlag = logFlag;
}

public synchronized boolean tryAcquire(int limitCount, long reqTime) {
this.setLastReqTime(reqTime);
this.slideWindow(reqTime);

if (limitCount >= 0 && this.calcTotalCount() >= limitCount) {
return false;
}
return true;
}

public void increase(long reqTime) {
// 当前时间对应的桶++
int index = (int) ((reqTime / bucketSizeMs) % bucketCount);
buckets[index].incrementAndGet();
// print buckets
log.debug("{} buckets: {}", logFlag, Arrays.toString(buckets));
}

/**
* 滑动窗口更新:清理过期桶
*/
private void slideWindow(long reqTime) {
// 计算请求时间的全局桶索引
long globalBucketIndex = reqTime / bucketSizeMs;
long globalBucketsPassed = globalBucketIndex - lastGlobalBucketIndex;

// log
log.debug("{} slideWindow: reqTime={}, globalBucketIndex={}, globalBucketsPassed={}, lastGlobalBucketIndex={}",
logFlag, reqTime, globalBucketIndex, globalBucketsPassed, lastGlobalBucketIndex);

// 如果时间没有过去一个桶的大小,直接返回
if (globalBucketsPassed <= 0) {
log.debug("{}, no need to slide", logFlag);
return;
}

// 如果时间过去超过所有桶,则全部过期
if (globalBucketsPassed >= bucketCount) {
log.debug("{} clear all", logFlag);
for (AtomicInteger b : buckets) {
b.set(0);
}
} else {
// 从lastBucketIndex后一个开始正向清除bucketsPassed个桶
for (int i = 1; i <= globalBucketsPassed; i++) {
int index = (int) ((lastGlobalBucketIndex + i) % bucketCount);
log.debug("{} clear index: {}", logFlag, index);
buckets[index].set(0);
}
}

lastUpdateTime = reqTime;
lastGlobalBucketIndex = reqTime / bucketSizeMs;
log.debug("{} slideWindow: lastUpdateTime={}, lastGlobalBucketIndex={}", logFlag, lastUpdateTime, lastGlobalBucketIndex);
}

/**
* 统计所有桶总和
*/
private int calcTotalCount() {
int total = 0;
for (AtomicInteger b : buckets) {
total += b.get();
}
return total;
}
}

/**
* 清理过期的 group,防止 innerGroupHolder 无限制增长
*/
private void cleanExpiredGroups() {
long now = System.currentTimeMillis();

try {
log.debug("[RateLimitCleaner] 开始清理过期 group... now={}", now);

innerGroupHolder.forEach((resName, groupMap) -> {
// 遍历 groupMap,但删除时使用 CAS,不直接 removeIf
for (Map.Entry<String, SlidingWindow> entry : groupMap.entrySet()) {
String group = entry.getKey();
SlidingWindow window = entry.getValue();

long lastReq = window.getLastReqTime();
boolean expired = (now - lastReq > GROUP_EXPIRE_MS) && (now - lastReq > window.windowSizeMs);
log.debug("[RateLimitCleaner] group={} (resName={}) lastReq={}, expiredMs={}, GROUP_EXPIRE_MS={}, windowSizeMs={}",
group, resName, lastReq, now - lastReq, GROUP_EXPIRE_MS, window.windowSizeMs);
if (expired) {
// CAS 删除:确保删除的是当前 value,防止误删
boolean removed = groupMap.remove(group, window);
if (removed) {
log.debug("[RateLimitCleaner] 已清理 group={} (resName={}) lastReq={}, expiredMs={}",
group, resName, lastReq, now - lastReq);
}
}
}
});

} catch (Exception e) {
log.error("[RateLimitCleaner] 清理异常", e);
}
}

/**
* 获取分组名称
*/
private String getGroupName(RateLimitConf rlConf, Method m, HttpServletRequest request) {
switch (rlConf.getGroupType()) {
case IP:
return request.getRemoteAddr();
case CUSTOM:
return this.getCustomGroupName(rlConf.getCustomGroup(), request, m);
case NONE:
default:
return String.valueOf(rlConf.getWindowFrom());
}
}

/**
* 获取自定义分组名称
* 支持:
* 1. 普通字符串(不以 # 开头),直接返回
* 2. SpEL 表达式(以 # 开头),解析返回
*/
private String getCustomGroupName(String customGroup, HttpServletRequest request, Method method) {
if (customGroup == null || customGroup.trim().isEmpty()) {
log.warn("自定义分组名称不能为空");
throw new RuntimeException("自定义分组名称不能为空");
}

// 1. 普通字符串,直接返回
if (!customGroup.trim().startsWith("#")) {
return customGroup;
}

// 2. SpEL 表达式 —— 动态解析
try {
// 上下文变量,可扩展
CONTEXT.setVariable("request", request);
CONTEXT.setVariable("method", method);

// rls = Rate Limit Service
CONTEXT.setVariable("rls", rateLimitService);

Object val = PARSER.parseExpression(customGroup).getValue(CONTEXT);
if (val == null) {
log.warn("解析分组名称失败");
throw new RuntimeException("解析分组名称失败");
}
log.debug("SpEL 解析分组名称成功:{}", val);
return String.valueOf(val);
} catch (Exception e) {
log.warn("SpEL 解析分组名称失败");
throw new RuntimeException("解析分组名称失败");
}
}
}

其他的一些辅助类#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.yeshimin.yeahboot.flowcontrol.ratelimit;

import com.yeshimin.yeahboot.flowcontrol.enums.GroupType;
import lombok.Data;

/**
* RateLimit 配置类
*/
@Data
class RateLimitConf {

private boolean enabled;

private String name;

private GroupType groupType;

private String customGroup;

private int limitCount;

private int limitGroup;

private int limitGroupCount;

// 时间窗口,单位:毫秒
private int timeWindow;

// 时间窗口内桶大小,单位:毫秒
private int bucketSize;

private boolean dynamicTimeWindow;

private boolean global;

// --------------------------------------------------------------------------------

// 计算得出
private long windowFrom;

// 计算得出
private long windowTo;

// 请求时间
private long reqTime;

// 计算得出
private String resName;

// 计算得出
private String groupName;

public boolean isSkip() {
return !enabled || (limitCount < 0 && limitGroup < 0 && limitGroupCount < 0);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.yeshimin.yeahboot.flowcontrol.ratelimit;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

public interface RateLimitService {

String getGroupName(HttpServletRequest request, Method method);

String getUserId(HttpServletRequest request, Method method);

String getIp(HttpServletRequest request, Method method);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.yeshimin.yeahboot.flowcontrol.enums;

import lombok.Getter;

/**
* 分组方式:1-无 2-按IP 3-自定义
*/
@Getter
public enum GroupType {

NONE(1),
IP(2),
CUSTOM(3);

private final Integer value;

GroupType(Integer value) {
this.value = value;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.yeshimin.yeahboot.flowcontrol;

import com.yeshimin.yeahboot.flowcontrol.ratelimit.RateLimitInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class FlowControlWebMvcConfig implements WebMvcConfigurer {

private final RateLimitInterceptor rateLimitInterceptor;

public FlowControlWebMvcConfig(RateLimitInterceptor rateLimitInterceptor) {
this.rateLimitInterceptor = rateLimitInterceptor;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/**");
}
}

测试#

此处只贴上【非分组方式的限流】的测试结果。

限制2秒内最多5个请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% curl 'http://localhost:8080/app/demo/ratelimit/test'
hello world

% curl 'http://localhost:8080/app/demo/ratelimit/test'
hello world

% curl 'http://localhost:8080/app/demo/ratelimit/test'
hello world

% curl 'http://localhost:8080/app/demo/ratelimit/test'
hello world

% curl 'http://localhost:8080/app/demo/ratelimit/test'
hello world

% curl 'http://localhost:8080/app/demo/ratelimit/test'
Rate limit exceeded: limitCount

% curl 'http://localhost:8080/app/demo/ratelimit/test'
Rate limit exceeded: limitCount

后续#

目前够用,后续按需扩展功能;
比如目前是单机,后续支持global分布式限流。

项目地址#