mirror of
https://github.com/zongzibinbin/MallChat.git
synced 2025-12-26 04:47:53 +08:00
add FrequencyControl
This commit is contained in:
parent
81d60f6453
commit
0564483102
43
frequency-control/.gitignore
vendored
Normal file
43
frequency-control/.gitignore
vendored
Normal 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
46
frequency-control/pom.xml
Normal 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>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package com.abin.frequencycontrol.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 限流策略定义
|
||||
*/
|
||||
@Data
|
||||
public class FixedWindowDTO extends FrequencyControlDTO {
|
||||
|
||||
/**
|
||||
* 频控时间范围,默认单位秒
|
||||
*
|
||||
* @return 时间范围
|
||||
*/
|
||||
private Integer time;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.abin.frequencycontrol.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* web请求信息收集类
|
||||
*/
|
||||
@Data
|
||||
public class RequestInfo {
|
||||
private Long uid;
|
||||
private String ip;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.abin.frequencycontrol.exception;
|
||||
|
||||
public interface ErrorEnum {
|
||||
|
||||
Integer getErrorCode();
|
||||
|
||||
String getErrorMsg();
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user