注解实现接口频率控制

MallChat 项目过程中学习到的注解实现方式

太难懂了太难懂了… 三个多月没摸项目, 现在正好有地方要用到频率控制的东西, 想着MallChat里有注解式实现的组件(防止用户刷屏)回来看看。不看不要紧, 一看两三天就这么过去了!

注解实现

注解类

首先, 要对某一个接口, 或者说是用户要想拦截他的请求实现频率控制需要考虑有哪些参数:

  1. 在限制对象方面, 目标是实现用户对接口的频控, 所以需要的入参有两个: 方法名标识用户身份的属性, 例如 uidip (当然, 在MallChat还额外添加了以 SpEL[1] 辨识用户的方式)
  2. 在频率控制方面需要的参数就很简单: 限制时间, 时间单位, 限制次数

其次, 注解应该可重复执行, 例如对某一接口, 在限制用户 id 的同时一起限制其 ip.
综上, 注解的编写如下:

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
/**
* 频控注解
*/
@Repeatable(FrequencyControlContainer.class)//可重复
@Retention(RetentionPolicy.RUNTIME)//运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface FrequencyControl {
/**
* key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做频控,就自己指定
*
* @return key的前缀
*/
String prefixKey() default "";

/**
* 频控对象,默认el表达指定具体的频控对象
* 对于ip 和uid模式,需要是http入口的对象,保证RequestHolder里有值
*
* @return 对象
*/
Target target() default Target.EL;

/**
* springEl 表达式,target=EL必填
*
* @return 表达式
*/
String spEl() default "";

/**
* 频控时间范围,默认单位秒
*
* @return 时间范围
*/
int time();

/**
* 频控时间单位,默认秒
*
* @return 单位
*/
TimeUnit unit() default TimeUnit.SECONDS;

/**
* 单位时间内最大访问次数
*
* @return 次数
*/
int count();

enum Target {
UID, IP, EL
}
}

切面实现注解

在切面类中的实现其实很简单: 在注解处获取信息, 然后依据设定的控制目标获取存入redis中的 key (方法名 + 频控目标), 最后连同选定的频控策略 TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER(此处为固定时间内频控) 一并传给具体实现频控的工具类就行

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
@Slf4j
@Aspect
@Component
public class FrequencyControlAspect {

@Around("@annotation(org.example.common.annotation.FrequencyControl)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
FrequencyControl[] annotationsByType = method.getAnnotationsByType(FrequencyControl.class);
Map<String, FrequencyControl> keyMap = new HashMap<>();
for (int i = 0; i < annotationsByType.length; i++) {
FrequencyControl frequencyControl = annotationsByType[i];
String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? SpElUtils.getMethodKey(method) + ":index:" + i : frequencyControl.prefixKey();//默认方法限定名+注解排名(可能多个)
String key = "";
switch (frequencyControl.target()) {
case EL:
key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl());
break;
case IP:
key = RequestHolder.get().getIp();
break;
// case UID:// 测试时简单接口无id,直接注释掉id的部分了
// key = RequestHolder.get().getIp();;
}
keyMap.put(prefix + ":" + key, frequencyControl);
}
// 将注解的参数转换为编程式调用需要的参数
List<FrequencyControlDTO> frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildFrequencyControlDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList());
// 调用编程式注解
return FrequencyControlUtil.executeWithFrequencyControlList(TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER, frequencyControlDTOS, joinPoint::proceed);
}

/**
* 将注解参数转换为编程式调用所需要的参数
*
* @param key 频率控制Key
* @param frequencyControl 注解
* @return 编程式调用所需要的参数-FrequencyControlDTO
*/
private FrequencyControlDTO buildFrequencyControlDTO(String key, FrequencyControl frequencyControl) {
FrequencyControlDTO frequencyControlDTO = new FrequencyControlDTO();
frequencyControlDTO.setCount(frequencyControl.count());
frequencyControlDTO.setTime(frequencyControl.time());
frequencyControlDTO.setUnit(frequencyControl.unit());
frequencyControlDTO.setKey(key);
return frequencyControlDTO;
}
}
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
@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
/** 限流策略定义
* @author linzhihan
* @date 2023/07/03
*
*/
public class FrequencyControlDTO {
/**
* 代表频控的Key 如果target为Key的话 这里要传值用于构建redis的Key target为Ip或者UID的话会从上下文取值 Key字段无需传值
*/
private String key;
/**
* 频控时间范围,默认单位秒
*
* @return 时间范围
*/
private Integer time;

/**
* 频控时间单位,默认秒
*
* @return 单位
*/
private TimeUnit unit;

/**
* 单位时间内最大访问次数
*
* @return 次数
*/
private Integer count;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SpElUtils {
private static final ExpressionParser parser = new SpelExpressionParser();
private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

public static String parseSpEl(Method method, Object[] args, String spEl) {
String[] params = Optional.ofNullable(parameterNameDiscoverer.getParameterNames(method)).orElse(new String[]{});//解析参数名
EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去
}
Expression expression = parser.parseExpression(spEl);
return expression.getValue(context, String.class);
}

public static String getMethodKey(Method method) {
return method.getDeclaringClass() + "#" + method.getName();
}
}

频率控制的编程式实现

众所周知, 频率控制有三种实现方式

  • 固定时间窗口(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
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
/**
* 抽象类频控服务 -使用redis实现 固定时间内不超过固定次数的限流类
*
* @author linzhihan
* @date 2023/07/03
*/
@Slf4j
@Service
public class TotalCountWithInFixTimeFrequencyController extends AbstractFrequencyControlService<FrequencyControlDTO> {


/**
* 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
*
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
* @return true-方法被限流 false-方法没有被限流
*/
@Override
protected boolean reachRateLimit(Map<String, FrequencyControlDTO> frequencyControlMap) {
//批量获取redis统计的值
List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
List<Integer> countList = RedisUtils.mget(frequencyKeys, Integer.class);
for (int i = 0; i < frequencyKeys.size(); i++) {
String key = frequencyKeys.get(i);
Integer count = countList.get(i);
int frequencyControlCount = frequencyControlMap.get(key).getCount();
if (Objects.nonNull(count) && count >= frequencyControlCount) {
//频率超过了
log.warn("frequencyControl limit key:{},count:{}", key, count);
return true;
}
}
return false;
}

/**
* 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑
*
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
*/
@Override
protected void addFrequencyControlStatisticsCount(Map<String, FrequencyControlDTO> frequencyControlMap) {
frequencyControlMap.forEach((k, v) -> RedisUtils.inc(k, v.getTime(), v.getUnit()));
}

@Override
protected String getStrategyName() {
return TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
}
}

工具类参见MallChat源码:
JsonUtils
RedisUtils
至此, 基于固定时间窗口实现接口频率控制的注解就完成了

测试

建立 SpringBoot 项目并导入 Maven 依赖:

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.15</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.15</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.15</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
</dependency>
</dependencies>

通过 ThreadLocal 以及 拦截器的方式获取对接口请求的ip

拦截器

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
@Component
public class CollectorInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 在接收请求时使用Token拦截器获取token和ip信息
// Long uid = Optional.ofNullable(request.getAttribute(TokenInterceptor.UID))
// .map(Object::toString)
// .map(Long::parseLong).orElse(null);

String ip = ServletUtil.getClientIP(request);

RequestInfo requestInfo = new RequestInfo();
// requestInfo.setUid(uid);
requestInfo.setIp(ip);

RequestHolder.set(requestInfo);
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
RequestHolder.remove(); // localThread线程释放
}
}

RequestHolder类以及RequestInfo类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 请求上下文
*/
public class RequestHolder {

private static final ThreadLocal<RequestInfo> threadlocal = new ThreadLocal<RequestInfo>();
public static void set(RequestInfo requestInfo) {
threadlocal.set(requestInfo);
}

public static RequestInfo get() {
return threadlocal.get();
}

public static void remove() {
threadlocal.remove();
}
}
1
2
3
4
5
@Data
public class RequestInfo {
private Long uid;
public String ip;
}

启用拦截器

1
2
3
4
5
6
7
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CollectorInterceptor()).addPathPatterns("/**");
}
}

编写测试接口

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class FrequencyController {

@GetMapping("/test")
@FrequencyControl(time = 120, count = 3, target = FrequencyControl.Target.IP)
public String test() {
String ip = RequestHolder.get().getIp();
System.out.println(ip);
return "这是一个简易的测试接口返回信息";
}
}

测试接口并查看Redis中存储的键值:
接口测试
Redis键值

参考资料


注解实现接口频率控制
http://example.com/2025/03/19/注解实现接口频率控制/
作者
Emberff
发布于
2025年3月19日
许可协议