注解实现接口频率控制
在MallChat 项目过程中学习到的注解实现方式
太难懂了太难懂了… 三个多月没摸项目, 现在正好有地方要用到频率控制的东西, 想着MallChat里有注解式实现的组件(防止用户刷屏)回来看看。不看不要紧, 一看两三天就这么过去了!
注解实现
注解类
首先, 要对某一个接口, 或者说是用户要想拦截他的请求实现频率控制需要考虑有哪些参数:
- 在限制对象方面, 目标是实现用户对接口的频控, 所以需要的入参有两个:
方法名
和标识用户身份的属性
, 例如uid
或ip
(当然, 在MallChat还额外添加了以SpEL
[1] 辨识用户的方式) - 在频率控制方面需要的参数就很简单:
限制时间
,时间单位
,限制次数
其次, 注解应该可重复执行, 例如对某一接口, 在限制用户 id
的同时一起限制其 ip
.
综上, 注解的编写如下:
1 |
|
切面实现注解
在切面类中的实现其实很简单: 在注解处获取信息, 然后依据设定的控制目标获取存入redis中的 key
(方法名 + 频控目标)
, 最后连同选定的频控策略 TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER
(此处为固定时间内频控) 一并传给具体实现频控的工具类就行
1 |
|
1 |
|
1 |
|
频率控制的编程式实现
众所周知, 频率控制有三种实现方式
- 固定时间窗口(Fixed Window)
- 滑动窗口(Sliding Window)
- 令牌桶(Token Bucket)
此处的频率控制是基于 固定时间窗口实现的
.
1. 工具类 (FrequencyControlUtil)
作用:
- 作为入口封装,让调用方无需关心具体的限流策略实现。
- 统一对不同的限流策略(如固定窗口、滑动窗口、令牌桶等)进行调用。
- 隐藏复杂性,调用方只需要传递策略名称和限流参数,无需直接操作 Redis 或具体策略逻辑。
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/**
* 限流工具类 提供编程式的限流调用方法
*
* @author linzhihan
* @date 2023/07/03
*/
public class FrequencyControlUtil {
/**
* 单限流策略的调用方法-编程式调用
*
* @param strategyName 策略名称
* @param frequencyControl 单个频控对象
* @param supplier 服务提供着
* @return 业务方法执行结果
* @throws Throwable
*/
public static <T, K extends FrequencyControlDTO> T executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.SupplierThrowWithoutParam<T> supplier) throws Throwable {
AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
return frequencyController.executeWithFrequencyControl(frequencyControl, supplier);
}
public static <K extends FrequencyControlDTO> void executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.Executor executor) throws Throwable {
AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
frequencyController.executeWithFrequencyControl(frequencyControl, () -> {
executor.execute();
return null;
});
}
/**
* 多限流策略的编程式调用方法调用方法
*
* @param strategyName 策略名称
* @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序
* @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑
* @return 业务方法执行的返回值
* @throws Throwable 被限流或者限流策略定义错误
*/
public static <T, K extends FrequencyControlDTO> T executeWithFrequencyControlList(String strategyName, List<K> frequencyControlList, AbstractFrequencyControlService.SupplierThrowWithoutParam<T> supplier) throws Throwable {
boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
if (existsFrequencyControlHasNullKey) {
throw new RuntimeException("限流策略的Key字段不允许出现空值");
}
AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
return frequencyController.executeWithFrequencyControlList(frequencyControlList, supplier);
}
/**
* 构造器私有
*/
private FrequencyControlUtil() {
}
}
2. 服务类(AbstractFrequencyControlService)
- 负责真正的限流逻辑实现,比如如何判断达到阈值(reachRateLimit)以及如何记录统计次数(addFrequencyControlStatisticsCount)。
- 采用抽象类,为不同的限流策略(固定窗口、滑动窗口、令牌桶)提供一个统一的模板。
- 通过策略模式,每种限流方式(例如TotalCountWithInFixTimeFrequencyController)都可以单独实现自己的逻辑,而不影响其他策略。
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/**
* 抽象类频控服务 其他类如果要实现限流服务 直接注入使用通用限流类
* 后期会通过继承此类实现令牌桶等算法
*
* @author linzhihan
* @date 2023/07/03
* @see TotalCountWithInFixTimeFrequencyController 通用限流类
*/
@Slf4j
public abstract class AbstractFrequencyControlService<K extends FrequencyControlDTO> {
@PostConstruct
protected void registerMyselfToFactory() {
FrequencyControlStrategyFactory.registerFrequencyController(getStrategyName(), this);
}
/**
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
* @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑
* @return 业务方法执行的返回值
* @throws Throwable
*/
private <T> T executeWithFrequencyControlMap(Map<String, K> frequencyControlMap, SupplierThrowWithoutParam<T> supplier) throws Throwable {
if (reachRateLimit(frequencyControlMap)) {
throw new Exception("次数限制!");
}
try {
return supplier.get();
} finally {
//不管成功还是失败,都增加次数
addFrequencyControlStatisticsCount(frequencyControlMap);
}
}
/**
* 多限流策略的编程式调用方法 无参的调用方法
*
* @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序
* @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑
* @return 业务方法执行的返回值
* @throws Throwable 被限流或者限流策略定义错误
*/
@SuppressWarnings("unchecked")
public <T> T executeWithFrequencyControlList(List<K> frequencyControlList, SupplierThrowWithoutParam<T> supplier) throws Throwable {
boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
if (existsFrequencyControlHasNullKey) {
throw new RuntimeException("限流策略的Key字段不允许出现空值");
}
Map<String, FrequencyControlDTO> frequencyControlDTOMap = frequencyControlList.stream().collect(Collectors.groupingBy(FrequencyControlDTO::getKey, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
return executeWithFrequencyControlMap((Map<String, K>) frequencyControlDTOMap, supplier);
}
/**
* 单限流策略的调用方法-编程式调用
*
* @param frequencyControl 单个频控对象
* @param supplier 服务提供着
* @return 业务方法执行结果
* @throws Throwable
*/
public <T> T executeWithFrequencyControl(K frequencyControl, SupplierThrowWithoutParam<T> supplier) throws Throwable {
return executeWithFrequencyControlList(Collections.singletonList(frequencyControl), supplier);
}
@FunctionalInterface
public interface SupplierThrowWithoutParam<T> {
/**
* Gets a result.
*
* @return a result
*/
T get() throws Throwable;
}
@FunctionalInterface
public interface Executor {
/**
* Gets a result.
*
* @return a result
*/
void execute() throws Throwable;
}
/**
* 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
*
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
* @return true-方法被限流 false-方法没有被限流
*/
protected abstract boolean reachRateLimit(Map<String, K> frequencyControlMap);
/**
* 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑
*
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
*/
protected abstract void addFrequencyControlStatisticsCount(Map<String, K> frequencyControlMap);
/**
* 获取策略名称
*
* @return 策略名称
*/
protected abstract String getStrategyName();
}
3. 固定时间窗口的实现
通过继承抽象类具体实现方法
1 |
|
工具类参见MallChat源码:
JsonUtils
RedisUtils
至此, 基于固定时间窗口实现接口频率控制的注解就完成了
测试
建立 SpringBoot
项目并导入 Maven
依赖:
1 |
|
通过 ThreadLocal 以及 拦截器的方式获取对接口请求的ip
拦截器
1 |
|
RequestHolder类以及RequestInfo类
1 |
|
1 |
|
启用拦截器
1 |
|
编写测试接口
1 |
|
测试接口并查看Redis中存储的键值:
参考资料
注解实现接口频率控制
http://example.com/2025/03/19/注解实现接口频率控制/