add FrequencyControl

This commit is contained in:
zengwei-cogiot 2023-09-11 18:12:34 +08:00
parent 81d60f6453
commit 0564483102
32 changed files with 2647 additions and 0 deletions

43
frequency-control/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
error/
info/
warn/
### Project ###
temp/
*.log
### Eclipse ###
.metadata
.DS_Store
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

46
frequency-control/pom.xml Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mallchat</artifactId>
<groupId>com.abin.mallchat</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>frequency-control</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.abin.mallchat</groupId>
<artifactId>mallchat-common-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.4.3</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,16 @@
package com.abin.frequencycontrol;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@ServletComponentScan
public class FrequencyControlApplication {
public static void main(String[] args) {
SpringApplication.run(FrequencyControlApplication.class, args);
}
}

View File

@ -0,0 +1,84 @@
package com.abin.frequencycontrol.annotation;
import com.abin.mallchat.common.FrequencyControlConstant;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 频控注解
*/
@Repeatable(FrequencyControlContainer.class) // 可重复
@Retention(RetentionPolicy.RUNTIME)// 运行时生效
@Target(ElementType.METHOD)//作用在方法上
public @interface FrequencyControl {
/**
* 策略
*/
String strategy() default FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
/**
* 窗口大小默认 5 period
*/
int windowSize() default 5;
/**
* 窗口最小周期 1s (窗口大小是 5s 1s一个小格子共10个格子)
*/
int period() default 1;
/**
* 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() default 10;
/**
* 频控时间单位默认秒
*
* @return 单位
*/
TimeUnit unit() default TimeUnit.SECONDS;
/**
* 单位时间内最大访问次数
*
* @return 次数
*/
int count() default 1;
long capacity() default 3; // 令牌桶容量
double refillRate() default 0.5; // 每秒补充的令牌数
enum Target {
UID,
IP,
EL
}
}

View File

@ -0,0 +1,12 @@
package com.abin.frequencycontrol.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)// 运行时生效
public @interface FrequencyControlContainer {
FrequencyControl[] value();
}

View File

@ -0,0 +1,121 @@
package com.abin.frequencycontrol.aspect;
import cn.hutool.core.util.StrUtil;
import com.abin.frequencycontrol.util.RequestHolder;
import com.abin.mallchat.common.FrequencyControlConstant;
import com.abin.mallchat.utils.SpElUtils;
import com.abin.frequencycontrol.annotation.FrequencyControl;
import com.abin.frequencycontrol.domain.dto.FixedWindowDTO;
import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO;
import com.abin.frequencycontrol.domain.dto.SlidingWindowDTO;
import com.abin.frequencycontrol.domain.dto.TokenBucketDTO;
import com.abin.frequencycontrol.service.frequencycontrol.FrequencyControlUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 频控实现
*/
@Slf4j
@Aspect
@Component
public class FrequencyControlAspect {
@Around("@annotation(com.abin.frequencycontrol.annotation.FrequencyControl)||@annotation(com.abin.frequencycontrol.annotation.FrequencyControlContainer)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("FrequencyControlAspect start");
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
FrequencyControl[] annotationsByType = method.getAnnotationsByType(FrequencyControl.class);
Map<String, FrequencyControl> keyMap = new HashMap<>();
String strategy = FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
for (int i = 0; i < annotationsByType.length; i++) {
// 获取频控注解
FrequencyControl frequencyControl = annotationsByType[i];
String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? /* 默认方法限定名 + 注解排名(可能多个)*/method.toGenericString() + ":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:
key = RequestHolder.get().getUid().toString();
}
keyMap.put(prefix + ":" + key, frequencyControl);
strategy = frequencyControl.strategy();
}
// 将注解的参数转换为编程式调用需要的参数
if (FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER.equals(strategy)) {
// 调用编程式注解 固定窗口
List<FrequencyControlDTO> frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildFixedWindowDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList());
return FrequencyControlUtil.executeWithFrequencyControlList(strategy, frequencyControlDTOS, joinPoint::proceed);
} else if (FrequencyControlConstant.TOKEN_BUCKET_FREQUENCY_CONTROLLER.equals(strategy)) {
// 调用编程式注解 令牌桶
List<TokenBucketDTO> frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildTokenBucketDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList());
return FrequencyControlUtil.executeWithFrequencyControlList(strategy, frequencyControlDTOS, joinPoint::proceed);
} else {
// 调用编程式注解 滑动窗口
List<SlidingWindowDTO> frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildSlidingWindowFrequencyControlDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList());
return FrequencyControlUtil.executeWithFrequencyControlList(strategy, frequencyControlDTOS, joinPoint::proceed);
}
}
/**
* 将注解参数转换为编程式调用所需要的参数
*
* @param key 频率控制Key
* @param frequencyControl 注解
* @return 编程式调用所需要的参数-FrequencyControlDTO
*/
private SlidingWindowDTO buildSlidingWindowFrequencyControlDTO(String key, FrequencyControl frequencyControl) {
SlidingWindowDTO frequencyControlDTO = new SlidingWindowDTO();
frequencyControlDTO.setWindowSize(frequencyControl.windowSize());
frequencyControlDTO.setPeriod(frequencyControl.period());
frequencyControlDTO.setCount(frequencyControl.count());
frequencyControlDTO.setUnit(frequencyControl.unit());
frequencyControlDTO.setKey(key);
return frequencyControlDTO;
}
/**
* 将注解参数转换为编程式调用所需要的参数
*
* @param key 频率控制Key
* @param frequencyControl 注解
* @return 编程式调用所需要的参数-FrequencyControlDTO
*/
private TokenBucketDTO buildTokenBucketDTO(String key, FrequencyControl frequencyControl) {
TokenBucketDTO tokenBucketDTO = new TokenBucketDTO(frequencyControl.capacity(), frequencyControl.refillRate());
tokenBucketDTO.setKey(key);
return tokenBucketDTO;
}
/**
* 将注解参数转换为编程式调用所需要的参数
*
* @param key 频率控制Key
* @param frequencyControl 注解
* @return 编程式调用所需要的参数-FrequencyControlDTO
*/
private FixedWindowDTO buildFixedWindowDTO(String key, FrequencyControl frequencyControl) {
FixedWindowDTO fixedWindowDTO = new FixedWindowDTO();
fixedWindowDTO.setCount(frequencyControl.count());
fixedWindowDTO.setTime(frequencyControl.time());
fixedWindowDTO.setUnit(frequencyControl.unit());
fixedWindowDTO.setKey(key);
return fixedWindowDTO;
}
}

View File

@ -0,0 +1,17 @@
package com.abin.frequencycontrol.domain.dto;
import lombok.Data;
/**
* 限流策略定义
*/
@Data
public class FixedWindowDTO extends FrequencyControlDTO {
/**
* 频控时间范围默认单位秒
*
* @return 时间范围
*/
private Integer time;
}

View File

@ -0,0 +1,30 @@
package com.abin.frequencycontrol.domain.dto;
import lombok.Data;
import java.util.concurrent.TimeUnit;
/**
* 限流策略定义
*/
@Data
public class FrequencyControlDTO {
/**
* 代表频控的Key 如果target为Key的话 这里要传值用于构建redis的Key target为Ip或者UID的话会从上下文取值 Key字段无需传值
*/
private String key;
/**
* 频控时间单位默认秒
*
* @return 单位
*/
private TimeUnit unit;
/**
* 单位时间内最大访问次数
*
* @return 次数
*/
private Integer count;
}

View File

@ -0,0 +1,12 @@
package com.abin.frequencycontrol.domain.dto;
import lombok.Data;
/**
* web请求信息收集类
*/
@Data
public class RequestInfo {
private Long uid;
private String ip;
}

View File

@ -0,0 +1,20 @@
package com.abin.frequencycontrol.domain.dto;
import lombok.Data;
/**
* 限流策略定义
*/
@Data
public class SlidingWindowDTO extends FrequencyControlDTO {
/**
* 窗口大小默认 10 s
*/
private int windowSize;
/**
* 窗口最小周期 1s (窗口大小是 10s 1s一个小格子-共10个格子)
*/
private int period;
}

View File

@ -0,0 +1,67 @@
package com.abin.frequencycontrol.domain.dto;
import com.abin.frequencycontrol.exception.BusinessErrorEnum;
import com.abin.frequencycontrol.exception.BusinessException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
@Data
@Slf4j
public class TokenBucketDTO extends FrequencyControlDTO {
private final long capacity; // 令牌桶容量
private final double refillRate; // 每秒补充的令牌数
private double tokens; // 当前令牌数量
private long lastRefillTime; // 上次补充令牌的时间
private final ReentrantLock lock = new ReentrantLock();
public TokenBucketDTO(long capacity, double refillRate) {
if (capacity <= 0 || refillRate <= 0) {
throw new BusinessException(BusinessErrorEnum.CAPACITY_REFILL_ERROR);
}
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.nanoTime();
}
public boolean tryAcquire(int permits) {
lock.lock();
try {
refillTokens();
if (tokens < permits) {
return true;
}
return false;
} finally {
lock.unlock();
}
}
public void deductionToken(int permits) {
lock.lock();
try {
tokens -= permits;
} finally {
lock.unlock();
}
}
/**
* 补充令牌
*/
private void refillTokens() {
long currentTime = System.nanoTime();
// 转换为秒
double elapsedTime = (currentTime - lastRefillTime) / 1e9;
double tokensToAdd = elapsedTime * refillRate;
log.info("tokensToAdd is {}", tokensToAdd);
// 令牌总数不能超过令牌桶容量
tokens = Math.min(capacity, tokens + tokensToAdd);
log.info("current tokens is {}", tokens);
lastRefillTime = currentTime;
}
}

View File

@ -0,0 +1,56 @@
package com.abin.frequencycontrol.domain.vo.response;
import com.abin.frequencycontrol.exception.ErrorEnum;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 通用返回体
*/
@Data
@ApiModel("基础返回体")
public class ApiResult<T> {
@ApiModelProperty("成功标识true or false")
private Boolean success;
@ApiModelProperty("错误码")
private Integer errCode;
@ApiModelProperty("错误消息")
private String errMsg;
@ApiModelProperty("返回对象")
private T data;
public static <T> ApiResult<T> success() {
ApiResult<T> result = new ApiResult<T>();
result.setData(null);
result.setSuccess(Boolean.TRUE);
return result;
}
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<T>();
result.setData(data);
result.setSuccess(Boolean.TRUE);
return result;
}
public static <T> ApiResult<T> fail(Integer code, String msg) {
ApiResult<T> result = new ApiResult<T>();
result.setSuccess(Boolean.FALSE);
result.setErrCode(code);
result.setErrMsg(msg);
return result;
}
public static <T> ApiResult<T> fail(ErrorEnum errorEnum) {
ApiResult<T> result = new ApiResult<T>();
result.setSuccess(Boolean.FALSE);
result.setErrCode(errorEnum.getErrorCode());
result.setErrMsg(errorEnum.getErrorMsg());
return result;
}
public boolean isSuccess() {
return this.success;
}
}

View File

@ -0,0 +1,75 @@
package com.abin.frequencycontrol.domain.vo.response;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("基础翻页返回")
public class PageBaseResp<T> {
@ApiModelProperty("当前页数")
private Integer pageNo;
@ApiModelProperty("每页查询数量")
private Integer pageSize;
@ApiModelProperty("总记录数")
private Long totalRecords;
@ApiModelProperty("是否最后一页")
private Boolean isLast = Boolean.FALSE;
@ApiModelProperty("数据列表")
private List<T> list;
public static <T> PageBaseResp<T> empty() {
PageBaseResp<T> r = new PageBaseResp<>();
r.setPageNo(1);
r.setPageSize(0);
r.setIsLast(true);
r.setTotalRecords(0L);
r.setList(new ArrayList<>());
return r;
}
public static <T> PageBaseResp<T> init(Integer pageNo, Integer pageSize, Long totalRecords, Boolean isLast, List<T> list) {
return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLast, list);
}
public static <T> PageBaseResp<T> init(Integer pageNo, Integer pageSize, Long totalRecords, List<T> list) {
return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLastPage(totalRecords, pageNo, pageSize), list);
}
public static <T> PageBaseResp<T> init(IPage<T> page) {
return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), page.getRecords());
}
public static <T> PageBaseResp<T> init(IPage page, List<T> list) {
return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), list);
}
public static <T> PageBaseResp<T> init(PageBaseResp resp, List<T> list) {
return init(resp.getPageNo(), resp.getPageSize(), resp.getTotalRecords(), resp.getIsLast(), list);
}
/**
* 是否是最后一页
*/
public static Boolean isLastPage(long totalRecords, int pageNo, int pageSize) {
if (pageSize == 0) {
return false;
}
long pageTotal = totalRecords / pageSize + (totalRecords % pageSize == 0 ? 0 : 1);
return pageNo >= pageTotal ? true : false;
}
}

View File

@ -0,0 +1,30 @@
package com.abin.frequencycontrol.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum BusinessErrorEnum implements ErrorEnum {
//==================================common==================================
BUSINESS_ERROR(1001, "{0}"),
//==================================user==================================
//==================================chat==================================
SYSTEM_ERROR(1001, "系统出小差了,请稍后再试哦~~"),
CAPACITY_REFILL_ERROR(1001, "Capacity and refill rate must be positive"),
;
private Integer code;
private String msg;
@Override
public Integer getErrorCode() {
return code;
}
@Override
public String getErrorMsg() {
return msg;
}
}

View File

@ -0,0 +1,55 @@
package com.abin.frequencycontrol.exception;
import lombok.Data;
@Data
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
*  错误码
*/
protected Integer errorCode;
/**
*  错误信息
*/
protected String errorMsg;
public BusinessException() {
super();
}
public BusinessException(String errorMsg) {
super(errorMsg);
this.errorMsg = errorMsg;
}
public BusinessException(Integer errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(Integer errorCode, String errorMsg, Throwable cause) {
super(errorMsg, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BusinessException(ErrorEnum error) {
super(error.getErrorMsg());
this.errorCode = error.getErrorCode();
this.errorMsg = error.getErrorMsg();
}
@Override
public String getMessage() {
return errorMsg;
}
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}

View File

@ -0,0 +1,27 @@
package com.abin.frequencycontrol.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum CommonErrorEnum implements ErrorEnum {
SYSTEM_ERROR(-1, "系统出小差了,请稍后再试哦~~"),
PARAM_VALID(-2, "参数校验失败{0}"),
FREQUENCY_LIMIT(-3, "请求太频繁了,请稍后再试哦~~"),
LOCK_LIMIT(-4, "请求太频繁了,请稍后再试哦~~"),
;
private final Integer code;
private final String msg;
@Override
public Integer getErrorCode() {
return this.code;
}
@Override
public String getErrorMsg() {
return this.msg;
}
}

View File

@ -0,0 +1,8 @@
package com.abin.frequencycontrol.exception;
public interface ErrorEnum {
Integer getErrorCode();
String getErrorMsg();
}

View File

@ -0,0 +1,37 @@
package com.abin.frequencycontrol.exception;
import lombok.Data;
/**
* 自定义限流异常
*/
@Data
public class FrequencyControlException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
*  错误码
*/
protected Integer errorCode;
/**
*  错误信息
*/
protected String errorMsg;
public FrequencyControlException() {
super();
}
public FrequencyControlException(String errorMsg) {
super(errorMsg);
this.errorMsg = errorMsg;
}
public FrequencyControlException(ErrorEnum error) {
super(error.getErrorMsg());
this.errorCode = error.getErrorCode();
this.errorMsg = error.getErrorMsg();
}
}

View File

@ -0,0 +1,40 @@
package com.abin.frequencycontrol.exception;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import com.abin.frequencycontrol.domain.vo.response.ApiResult;
import lombok.AllArgsConstructor;
import lombok.Getter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.Charset;
/**
* Description: 业务校验异常码6
*/
@AllArgsConstructor
@Getter
public enum HttpErrorEnum implements ErrorEnum {
ACCESS_DENIED(401, "登录失效,请重新登录"),
;
private Integer httpCode;
private String msg;
@Override
public Integer getErrorCode() {
return httpCode;
}
@Override
public String getErrorMsg() {
return msg;
}
public void sendHttpError(HttpServletResponse response) throws IOException {
response.setStatus(this.getErrorCode());
ApiResult responseData = ApiResult.fail(this);
response.setContentType(ContentType.JSON.toString(Charset.forName("UTF-8")));
response.getWriter().write(JSONUtil.toJsonStr(responseData));
}
}

View File

@ -0,0 +1,51 @@
package com.abin.frequencycontrol.mannager;
import com.abin.frequencycontrol.domain.dto.TokenBucketDTO;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class TokenBucketManager {
private final Map<String, TokenBucketDTO> tokenBucketMap = new ConcurrentHashMap<>();
private final ReentrantLock lock = new ReentrantLock();
public void createTokenBucket(String key, long capacity, double refillRate) {
lock.lock();
try {
if (!tokenBucketMap.containsKey(key)) {
TokenBucketDTO tokenBucket = new TokenBucketDTO(capacity, refillRate);
tokenBucketMap.put(key, tokenBucket);
}
} finally {
lock.unlock();
}
}
public void removeTokenBucket(String key) {
lock.lock();
try {
tokenBucketMap.remove(key);
} finally {
lock.unlock();
}
}
public boolean tryAcquire(String key, int permits) {
TokenBucketDTO tokenBucket = tokenBucketMap.get(key);
if (tokenBucket != null) {
return tokenBucket.tryAcquire(permits);
}
return false;
}
public void deductionToken(String key, int permits) {
TokenBucketDTO tokenBucket = tokenBucketMap.get(key);
if (tokenBucket != null) {
tokenBucket.deductionToken(permits);
}
}
}

View File

@ -0,0 +1,121 @@
package com.abin.frequencycontrol.service.frequencycontrol;
import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO;
import com.abin.frequencycontrol.exception.CommonErrorEnum;
import com.abin.frequencycontrol.exception.FrequencyControlException;
import com.abin.frequencycontrol.util.AssertUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 抽象类频控服务 其他类如果要实现限流服务 直接注入使用通用限流类 后期会通过继承此类实现令牌桶等算法
*
* @param <K>
*/
@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 FrequencyControlException(CommonErrorEnum.FREQUENCY_LIMIT);
}
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()));
AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值");
Map<String, K> frequencyControlDTOMap = frequencyControlList.stream().collect(Collectors.groupingBy(K::getKey, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
return executeWithFrequencyControlMap(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();
}

View File

@ -0,0 +1,46 @@
package com.abin.frequencycontrol.service.frequencycontrol;
import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 限流策略工厂
*/
public class FrequencyControlStrategyFactory {
/**
* 限流策略集合
*/
static Map<String, AbstractFrequencyControlService<?>> frequencyControlServiceStrategyMap = new ConcurrentHashMap<>(8);
/**
* 将策略类放入工厂
*
* @param strategyName 策略名称
* @param abstractFrequencyControlService 策略类
*/
public static <K extends FrequencyControlDTO> void registerFrequencyController(String strategyName, AbstractFrequencyControlService<K> abstractFrequencyControlService) {
frequencyControlServiceStrategyMap.put(strategyName, abstractFrequencyControlService);
}
/**
* 根据名称获取策略类
*
* @param strategyName 策略名称
* @return 对应的限流策略类
*/
@SuppressWarnings("unchecked")
public static <K extends FrequencyControlDTO> AbstractFrequencyControlService<K> getFrequencyControllerByName(String strategyName) {
return (AbstractFrequencyControlService<K>) frequencyControlServiceStrategyMap.get(strategyName);
}
/**
* 构造器私有
*/
private FrequencyControlStrategyFactory() {
}
}

View File

@ -0,0 +1,60 @@
package com.abin.frequencycontrol.service.frequencycontrol;
import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO;
import com.abin.frequencycontrol.util.AssertUtil;
import org.apache.commons.lang3.ObjectUtils;
import java.util.List;
/**
* 限流工具类 提供编程式的限流调用方法
*/
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()));
AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值");
AbstractFrequencyControlService<K> frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
return frequencyController.executeWithFrequencyControlList(frequencyControlList, supplier);
}
/**
* 构造器私有
*/
private FrequencyControlUtil() {
}
}

View File

@ -0,0 +1,42 @@
package com.abin.frequencycontrol.service.frequencycontrol;
import java.util.function.Supplier;
public class GenericMethodWithGenericClass<T> {
public <E> T get(E value, Supplier<T> supplier) throws Throwable {
if (value == null) {
throw new Exception("Error");
}
try {
return supplier.get();
} finally {
// 不管成功还是失败都增加次数
System.out.println("execute");
}
}
// 泛型方法
public <E> void printArray(E[] array) {
for (E item : array) {
System.out.println(item);
}
}
public static void main(String[] args) {
GenericMethodWithGenericClass<Double> example = new GenericMethodWithGenericClass<>();
Integer[] intArray = {1, 2, 3, 4, 5};
String[] stringArray = {"Hello", "World"};
example.printArray(intArray);
example.printArray(stringArray);
try {
System.out.println(example.get("hello", Math::random));
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,64 @@
package com.abin.frequencycontrol.service.frequencycontrol.strategy;
import com.abin.mallchat.common.FrequencyControlConstant;
import com.abin.mallchat.utils.RedisUtils;
import com.abin.frequencycontrol.domain.dto.SlidingWindowDTO;
import com.abin.frequencycontrol.service.frequencycontrol.AbstractFrequencyControlService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 抽象类频控服务 -使用redis实现 滑动窗口是一种更加灵活的频率控制策略它在一个滑动的时间窗口内限制操作的发生次数
*/
@Slf4j
@Service
public class SlidingWindowFrequencyController extends AbstractFrequencyControlService<SlidingWindowDTO> {
@Override
protected boolean reachRateLimit(Map<String, SlidingWindowDTO> frequencyControlMap) {
// 批量获取redis统计的值
List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
for (int i = 0; i < frequencyKeys.size(); i++) {
String key = frequencyKeys.get(i);
SlidingWindowDTO controlDTO = frequencyControlMap.get(key);
// 获取窗口时间内计数
Long count = RedisUtils.ZSetGet(key);
int frequencyControlCount = controlDTO.getCount();
if (Objects.nonNull(count) && count >= frequencyControlCount) {
//频率超过了
log.warn("frequencyControl limit key:{},count:{}", key, count);
return true;
}
}
return false;
}
@Override
protected void addFrequencyControlStatisticsCount(Map<String, SlidingWindowDTO> frequencyControlMap) {
List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
for (int i = 0; i < frequencyKeys.size(); i++) {
String key = frequencyKeys.get(i);
SlidingWindowDTO controlDTO = frequencyControlMap.get(key);
// 窗口最小周期转秒
long period = controlDTO.getUnit().toMillis(controlDTO.getPeriod());
long current = System.currentTimeMillis();
// 窗口大小 单位
long length = period * controlDTO.getWindowSize();
long start = current - length;
// long expireTime = length + period;
RedisUtils.ZSetAddAndExpire(key, start, length, current);
}
}
@Override
protected String getStrategyName() {
return FrequencyControlConstant.SLIDING_WINDOW_FREQUENCY_CONTROLLER;
}
}

View File

@ -0,0 +1,54 @@
package com.abin.frequencycontrol.service.frequencycontrol.strategy;
import com.abin.mallchat.common.FrequencyControlConstant;
import com.abin.frequencycontrol.domain.dto.TokenBucketDTO;
import com.abin.frequencycontrol.mannager.TokenBucketManager;
import com.abin.frequencycontrol.service.frequencycontrol.AbstractFrequencyControlService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 抽象类频控服务 -使用redis实现 维护一个令牌桶来限制操作的发生次数
*/
@Slf4j
@Service
public class TokenBucketFrequencyController extends AbstractFrequencyControlService<TokenBucketDTO> {
@Autowired
private TokenBucketManager tokenBucketManager;
@Override
protected boolean reachRateLimit(Map<String, TokenBucketDTO> frequencyControlMap) {
// 批量获取redis统计的值
List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
for (int i = 0; i < frequencyKeys.size(); i++) {
String key = frequencyKeys.get(i);
// 获取 1 个令牌
return tokenBucketManager.tryAcquire(key, 1);
}
return false;
}
@Override
protected void addFrequencyControlStatisticsCount(Map<String, TokenBucketDTO> frequencyControlMap) {
List<String> frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
for (int i = 0; i < frequencyKeys.size(); i++) {
String key = frequencyKeys.get(i);
TokenBucketDTO tokenBucketDTO = frequencyControlMap.get(key);
tokenBucketManager.createTokenBucket(key, tokenBucketDTO.getCapacity(), tokenBucketDTO.getRefillRate());
// 扣减 1 个令牌
tokenBucketManager.deductionToken(key, 1);
}
}
@Override
protected String getStrategyName() {
return FrequencyControlConstant.TOKEN_BUCKET_FREQUENCY_CONTROLLER;
}
}

View File

@ -0,0 +1,63 @@
package com.abin.frequencycontrol.service.frequencycontrol.strategy;
import com.abin.mallchat.common.FrequencyControlConstant;
import com.abin.mallchat.utils.RedisUtils;
import com.abin.frequencycontrol.domain.dto.FixedWindowDTO;
import com.abin.frequencycontrol.service.frequencycontrol.AbstractFrequencyControlService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 抽象类频控服务 -使用redis实现 固定时间内不超过固定次数的限流类
*/
@Slf4j
@Service
public class TotalCountWithInFixTimeFrequencyController extends AbstractFrequencyControlService<FixedWindowDTO> {
/**
* 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
*
* @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
* @return true-方法被限流 false-方法没有被限流
*/
@Override
protected boolean reachRateLimit(Map<String, FixedWindowDTO> 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, FixedWindowDTO> frequencyControlMap) {
frequencyControlMap.forEach((k, v) -> RedisUtils.inc(k, v.getTime(), v.getUnit()));
}
@Override
protected String getStrategyName() {
return FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER;
}
}

View File

@ -0,0 +1,159 @@
package com.abin.frequencycontrol.util;
import cn.hutool.core.util.ObjectUtil;
import com.abin.frequencycontrol.exception.BusinessErrorEnum;
import com.abin.frequencycontrol.exception.BusinessException;
import com.abin.frequencycontrol.exception.CommonErrorEnum;
import com.abin.frequencycontrol.exception.ErrorEnum;
import org.hibernate.validator.HibernateValidator;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.text.MessageFormat;
import java.util.*;
/**
* 校验工具类
*/
public class AssertUtil {
/**
* 校验到失败就结束
*/
private static Validator failFastValidator = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory().getValidator();
/**
* 全部校验
*/
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
/**
* 注解验证参数(校验到失败就结束)
* @param obj
*/
public static <T> void fastFailValidate(T obj) {
Set<ConstraintViolation<T>> constraintViolations = failFastValidator.validate(obj);
if (constraintViolations.size() > 0) {
throwException(CommonErrorEnum.PARAM_VALID,constraintViolations.iterator().next().getMessage());
}
}
/**
* 注解验证参数(全部校验,抛出异常)
* @param obj
*/
public static <T> void allCheckValidateThrow(T obj) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj);
if (constraintViolations.size() > 0) {
StringBuilder errorMsg = new StringBuilder();
Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> violation = iterator.next();
//拼接异常信息
errorMsg.append(violation.getPropertyPath().toString()).append(":").append(violation.getMessage()).append(",");
}
//去掉最后一个逗号
throwException(CommonErrorEnum.PARAM_VALID, errorMsg.toString().substring(0, errorMsg.length() - 1));
}
}
/**
* 注解验证参数(全部校验,返回异常信息集合)
* @param obj
*/
public static <T> Map<String,String> allCheckValidate(T obj) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj);
if (constraintViolations.size() > 0) {
Map<String,String> errorMessages= new HashMap<>();
Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> violation = iterator.next();
errorMessages.put(violation.getPropertyPath().toString(),violation.getMessage());
}
return errorMessages;
}
return new HashMap<>();
}
//如果不是true则抛异常
public static void isTrue(boolean expression, String msg) {
if (!expression) {
throwException(msg);
}
}
public static void isTrue(boolean expression, ErrorEnum errorEnum, Object... args) {
if (!expression) {
throwException(errorEnum, args);
}
}
//如果是true则抛异常
public static void isFalse(boolean expression, String msg) {
if (expression) {
throwException(msg);
}
}
//如果是true则抛异常
public static void isFalse(boolean expression, ErrorEnum errorEnum, Object... args) {
if (expression) {
throwException(errorEnum, args);
}
}
//如果不是非空对象则抛异常
public static void isNotEmpty(Object obj, String msg) {
if (isEmpty(obj)) {
throwException(msg);
}
}
//如果不是非空对象则抛异常
public static void isNotEmpty(Object obj, ErrorEnum errorEnum, Object... args) {
if (isEmpty(obj)) {
throwException(errorEnum, args);
}
}
//如果不是非空对象则抛异常
public static void isEmpty(Object obj, String msg) {
if (!isEmpty(obj)) {
throwException(msg);
}
}
public static void equal(Object o1, Object o2, String msg) {
if (!ObjectUtil.equal(o1, o2)) {
throwException(msg);
}
}
public static void notEqual(Object o1, Object o2, String msg) {
if (ObjectUtil.equal(o1, o2)) {
throwException(msg);
}
}
private static boolean isEmpty(Object obj) {
return ObjectUtil.isEmpty(obj);
}
private static void throwException(String msg) {
throwException(null, msg);
}
private static void throwException(ErrorEnum errorEnum, Object... arg) {
if (Objects.isNull(errorEnum)) {
errorEnum = BusinessErrorEnum.BUSINESS_ERROR;
}
throw new BusinessException(errorEnum.getErrorCode(), MessageFormat.format(errorEnum.getErrorMsg(), arg));
}
}

View File

@ -0,0 +1,24 @@
package com.abin.frequencycontrol.util;
import com.abin.frequencycontrol.domain.dto.RequestInfo;
/**
* 请求上下文
*/
public class RequestHolder {
private static final ThreadLocal<RequestInfo> threadLocal = new ThreadLocal<>();
public static void set(RequestInfo requestInfo) {
threadLocal.set(requestInfo);
}
public static RequestInfo get() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}

View File

@ -0,0 +1,10 @@
package com.abin.mallchat.common;
public interface FrequencyControlConstant {
String TOTAL_COUNT_WITH_IN_FIX_TIME_FREQUENCY_CONTROLLER = "TotalCountWithInFixTime";
String SLIDING_WINDOW_FREQUENCY_CONTROLLER = "SlidingWindow";
String TOKEN_BUCKET_FREQUENCY_CONTROLLER = "TokenBucket";
}

View File

@ -0,0 +1,30 @@
package com.abin.mallchat.utils;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
import java.util.Optional;
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();
}
}