diff --git a/frequency-control/.gitignore b/frequency-control/.gitignore
new file mode 100644
index 0000000..edb1537
--- /dev/null
+++ b/frequency-control/.gitignore
@@ -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/
diff --git a/frequency-control/pom.xml b/frequency-control/pom.xml
new file mode 100644
index 0000000..8eb7060
--- /dev/null
+++ b/frequency-control/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+ mallchat
+ com.abin.mallchat
+ 1.0-SNAPSHOT
+
+ 4.0.0
+
+ frequency-control
+
+
+
+ org.projectlombok
+ lombok
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+ org.redisson
+ redisson-spring-boot-starter
+
+
+ com.abin.mallchat
+ mallchat-common-starter
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 2.4.3
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/FrequencyControlApplication.java b/frequency-control/src/main/java/com/abin/frequencycontrol/FrequencyControlApplication.java
new file mode 100644
index 0000000..18a8d54
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/FrequencyControlApplication.java
@@ -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);
+ }
+
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControl.java b/frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControl.java
new file mode 100644
index 0000000..ba6591d
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControl.java
@@ -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
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControlContainer.java b/frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControlContainer.java
new file mode 100644
index 0000000..8b251b0
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControlContainer.java
@@ -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();
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/aspect/FrequencyControlAspect.java b/frequency-control/src/main/java/com/abin/frequencycontrol/aspect/FrequencyControlAspect.java
new file mode 100644
index 0000000..7e77740
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/aspect/FrequencyControlAspect.java
@@ -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 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 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 frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildTokenBucketDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList());
+ return FrequencyControlUtil.executeWithFrequencyControlList(strategy, frequencyControlDTOS, joinPoint::proceed);
+ } else {
+ // 调用编程式注解 滑动窗口
+ List 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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FixedWindowDTO.java b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FixedWindowDTO.java
new file mode 100644
index 0000000..6235d9e
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FixedWindowDTO.java
@@ -0,0 +1,17 @@
+package com.abin.frequencycontrol.domain.dto;
+
+import lombok.Data;
+
+/**
+ * 限流策略定义
+ */
+@Data
+public class FixedWindowDTO extends FrequencyControlDTO {
+
+ /**
+ * 频控时间范围,默认单位秒
+ *
+ * @return 时间范围
+ */
+ private Integer time;
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FrequencyControlDTO.java b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FrequencyControlDTO.java
new file mode 100644
index 0000000..52ca8e9
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FrequencyControlDTO.java
@@ -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;
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/RequestInfo.java b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/RequestInfo.java
new file mode 100644
index 0000000..106db4e
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/RequestInfo.java
@@ -0,0 +1,12 @@
+package com.abin.frequencycontrol.domain.dto;
+
+import lombok.Data;
+
+/**
+ * web请求信息收集类
+ */
+@Data
+public class RequestInfo {
+ private Long uid;
+ private String ip;
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/SlidingWindowDTO.java b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/SlidingWindowDTO.java
new file mode 100644
index 0000000..abf0ea0
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/SlidingWindowDTO.java
@@ -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;
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/TokenBucketDTO.java b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/TokenBucketDTO.java
new file mode 100644
index 0000000..2ffaace
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/TokenBucketDTO.java
@@ -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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/ApiResult.java b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/ApiResult.java
new file mode 100644
index 0000000..75e3165
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/ApiResult.java
@@ -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 {
+ @ApiModelProperty("成功标识true or false")
+ private Boolean success;
+ @ApiModelProperty("错误码")
+ private Integer errCode;
+ @ApiModelProperty("错误消息")
+ private String errMsg;
+ @ApiModelProperty("返回对象")
+ private T data;
+
+ public static ApiResult success() {
+ ApiResult result = new ApiResult();
+ result.setData(null);
+ result.setSuccess(Boolean.TRUE);
+ return result;
+ }
+
+ public static ApiResult success(T data) {
+ ApiResult result = new ApiResult();
+ result.setData(data);
+ result.setSuccess(Boolean.TRUE);
+ return result;
+ }
+
+ public static ApiResult fail(Integer code, String msg) {
+ ApiResult result = new ApiResult();
+ result.setSuccess(Boolean.FALSE);
+ result.setErrCode(code);
+ result.setErrMsg(msg);
+ return result;
+ }
+
+ public static ApiResult fail(ErrorEnum errorEnum) {
+ ApiResult result = new ApiResult();
+ result.setSuccess(Boolean.FALSE);
+ result.setErrCode(errorEnum.getErrorCode());
+ result.setErrMsg(errorEnum.getErrorMsg());
+ return result;
+ }
+
+ public boolean isSuccess() {
+ return this.success;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/PageBaseResp.java b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/PageBaseResp.java
new file mode 100644
index 0000000..e88f988
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/PageBaseResp.java
@@ -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 {
+
+ @ApiModelProperty("当前页数")
+ private Integer pageNo;
+
+ @ApiModelProperty("每页查询数量")
+ private Integer pageSize;
+
+ @ApiModelProperty("总记录数")
+ private Long totalRecords;
+
+ @ApiModelProperty("是否最后一页")
+ private Boolean isLast = Boolean.FALSE;
+
+ @ApiModelProperty("数据列表")
+ private List list;
+
+
+ public static PageBaseResp empty() {
+ PageBaseResp r = new PageBaseResp<>();
+ r.setPageNo(1);
+ r.setPageSize(0);
+ r.setIsLast(true);
+ r.setTotalRecords(0L);
+ r.setList(new ArrayList<>());
+ return r;
+ }
+
+ public static PageBaseResp init(Integer pageNo, Integer pageSize, Long totalRecords, Boolean isLast, List list) {
+ return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLast, list);
+ }
+
+ public static PageBaseResp init(Integer pageNo, Integer pageSize, Long totalRecords, List list) {
+ return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLastPage(totalRecords, pageNo, pageSize), list);
+ }
+
+ public static PageBaseResp init(IPage page) {
+ return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), page.getRecords());
+ }
+
+ public static PageBaseResp init(IPage page, List list) {
+ return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), list);
+ }
+
+ public static PageBaseResp init(PageBaseResp resp, List 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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessErrorEnum.java b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessErrorEnum.java
new file mode 100644
index 0000000..4187d58
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessErrorEnum.java
@@ -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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessException.java b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessException.java
new file mode 100644
index 0000000..4675641
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessException.java
@@ -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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/exception/CommonErrorEnum.java b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/CommonErrorEnum.java
new file mode 100644
index 0000000..f7a1197
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/CommonErrorEnum.java
@@ -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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/exception/ErrorEnum.java b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/ErrorEnum.java
new file mode 100644
index 0000000..fc3f2a9
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/ErrorEnum.java
@@ -0,0 +1,8 @@
+package com.abin.frequencycontrol.exception;
+
+public interface ErrorEnum {
+
+ Integer getErrorCode();
+
+ String getErrorMsg();
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/exception/FrequencyControlException.java b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/FrequencyControlException.java
new file mode 100644
index 0000000..9491dd9
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/FrequencyControlException.java
@@ -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();
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/exception/HttpErrorEnum.java b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/HttpErrorEnum.java
new file mode 100644
index 0000000..61849f3
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/exception/HttpErrorEnum.java
@@ -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));
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/mannager/TokenBucketManager.java b/frequency-control/src/main/java/com/abin/frequencycontrol/mannager/TokenBucketManager.java
new file mode 100644
index 0000000..5fd990c
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/mannager/TokenBucketManager.java
@@ -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 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);
+ }
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/AbstractFrequencyControlService.java b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/AbstractFrequencyControlService.java
new file mode 100644
index 0000000..14cfdab
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/AbstractFrequencyControlService.java
@@ -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
+ */
+@Slf4j
+public abstract class AbstractFrequencyControlService {
+
+ @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 executeWithFrequencyControlMap(Map frequencyControlMap, SupplierThrowWithoutParam 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 executeWithFrequencyControlList(List frequencyControlList, SupplierThrowWithoutParam supplier) throws Throwable {
+ boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
+ AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值");
+ Map 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 executeWithFrequencyControl(K frequencyControl, SupplierThrowWithoutParam supplier) throws Throwable {
+ return executeWithFrequencyControlList(Collections.singletonList(frequencyControl), supplier);
+ }
+
+
+ @FunctionalInterface
+ public interface SupplierThrowWithoutParam {
+
+ /**
+ * 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 frequencyControlMap);
+
+ /**
+ * 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑
+ *
+ * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
+ */
+ protected abstract void addFrequencyControlStatisticsCount(Map frequencyControlMap);
+
+ /**
+ * 获取策略名称
+ *
+ * @return 策略名称
+ */
+ protected abstract String getStrategyName();
+
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlStrategyFactory.java b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlStrategyFactory.java
new file mode 100644
index 0000000..6e14dff
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlStrategyFactory.java
@@ -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> frequencyControlServiceStrategyMap = new ConcurrentHashMap<>(8);
+
+ /**
+ * 将策略类放入工厂
+ *
+ * @param strategyName 策略名称
+ * @param abstractFrequencyControlService 策略类
+ */
+ public static void registerFrequencyController(String strategyName, AbstractFrequencyControlService abstractFrequencyControlService) {
+ frequencyControlServiceStrategyMap.put(strategyName, abstractFrequencyControlService);
+ }
+
+ /**
+ * 根据名称获取策略类
+ *
+ * @param strategyName 策略名称
+ * @return 对应的限流策略类
+ */
+ @SuppressWarnings("unchecked")
+ public static AbstractFrequencyControlService getFrequencyControllerByName(String strategyName) {
+ return (AbstractFrequencyControlService) frequencyControlServiceStrategyMap.get(strategyName);
+ }
+
+ /**
+ * 构造器私有
+ */
+ private FrequencyControlStrategyFactory() {
+
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlUtil.java b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlUtil.java
new file mode 100644
index 0000000..cba9c52
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlUtil.java
@@ -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 executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.SupplierThrowWithoutParam supplier) throws Throwable {
+ AbstractFrequencyControlService frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
+ return frequencyController.executeWithFrequencyControl(frequencyControl, supplier);
+ }
+
+ public static void executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.Executor executor) throws Throwable {
+ AbstractFrequencyControlService frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
+ frequencyController.executeWithFrequencyControl(frequencyControl, () -> {
+ executor.execute();
+ return null;
+ });
+ }
+
+
+ /**
+ * 多限流策略的编程式调用方法调用方法
+ *
+ * @param strategyName 策略名称
+ * @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序
+ * @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑
+ * @return 业务方法执行的返回值
+ * @throws Throwable 被限流或者限流策略定义错误
+ */
+ public static T executeWithFrequencyControlList(String strategyName, List frequencyControlList, AbstractFrequencyControlService.SupplierThrowWithoutParam supplier) throws Throwable {
+ boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey()));
+ AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值");
+ AbstractFrequencyControlService frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName);
+ return frequencyController.executeWithFrequencyControlList(frequencyControlList, supplier);
+ }
+
+ /**
+ * 构造器私有
+ */
+ private FrequencyControlUtil() {
+
+ }
+
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/GenericMethodWithGenericClass.java b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/GenericMethodWithGenericClass.java
new file mode 100644
index 0000000..ff8d43b
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/GenericMethodWithGenericClass.java
@@ -0,0 +1,42 @@
+package com.abin.frequencycontrol.service.frequencycontrol;
+
+import java.util.function.Supplier;
+
+ public class GenericMethodWithGenericClass {
+
+ public T get(E value, Supplier supplier) throws Throwable {
+ if (value == null) {
+ throw new Exception("Error");
+ }
+ try {
+ return supplier.get();
+ } finally {
+ // 不管成功还是失败,都增加次数
+ System.out.println("execute");
+ }
+ }
+
+ // 泛型方法
+ public void printArray(E[] array) {
+ for (E item : array) {
+ System.out.println(item);
+ }
+ }
+
+
+ public static void main(String[] args) {
+ GenericMethodWithGenericClass 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);
+ }
+ }
+ }
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/SlidingWindowFrequencyController.java b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/SlidingWindowFrequencyController.java
new file mode 100644
index 0000000..be82e40
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/SlidingWindowFrequencyController.java
@@ -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 {
+ @Override
+ protected boolean reachRateLimit(Map frequencyControlMap) {
+ // 批量获取redis统计的值
+ List 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 frequencyControlMap) {
+ List 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;
+
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TokenBucketFrequencyController.java b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TokenBucketFrequencyController.java
new file mode 100644
index 0000000..8b9b6f4
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TokenBucketFrequencyController.java
@@ -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 {
+
+ @Autowired
+ private TokenBucketManager tokenBucketManager;
+
+ @Override
+ protected boolean reachRateLimit(Map frequencyControlMap) {
+ // 批量获取redis统计的值
+ List 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 frequencyControlMap) {
+ List 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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TotalCountWithInFixTimeFrequencyController.java b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TotalCountWithInFixTimeFrequencyController.java
new file mode 100644
index 0000000..652e994
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TotalCountWithInFixTimeFrequencyController.java
@@ -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 {
+
+
+ /**
+ * 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断
+ *
+ * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value
+ * @return true-方法被限流 false-方法没有被限流
+ */
+ @Override
+ protected boolean reachRateLimit(Map frequencyControlMap) {
+ //批量获取redis统计的值
+ List frequencyKeys = new ArrayList<>(frequencyControlMap.keySet());
+ List 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 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;
+ }
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/util/AssertUtil.java b/frequency-control/src/main/java/com/abin/frequencycontrol/util/AssertUtil.java
new file mode 100644
index 0000000..c4da534
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/util/AssertUtil.java
@@ -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 void fastFailValidate(T obj) {
+ Set> constraintViolations = failFastValidator.validate(obj);
+ if (constraintViolations.size() > 0) {
+ throwException(CommonErrorEnum.PARAM_VALID,constraintViolations.iterator().next().getMessage());
+ }
+ }
+
+ /**
+ * 注解验证参数(全部校验,抛出异常)
+ * @param obj
+ */
+ public static void allCheckValidateThrow(T obj) {
+ Set> constraintViolations = validator.validate(obj);
+ if (constraintViolations.size() > 0) {
+ StringBuilder errorMsg = new StringBuilder();
+ Iterator> iterator = constraintViolations.iterator();
+ while (iterator.hasNext()) {
+ ConstraintViolation 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 Map allCheckValidate(T obj) {
+ Set> constraintViolations = validator.validate(obj);
+ if (constraintViolations.size() > 0) {
+ Map errorMessages= new HashMap<>();
+ Iterator> iterator = constraintViolations.iterator();
+ while (iterator.hasNext()) {
+ ConstraintViolation 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));
+ }
+
+
+}
diff --git a/frequency-control/src/main/java/com/abin/frequencycontrol/util/RequestHolder.java b/frequency-control/src/main/java/com/abin/frequencycontrol/util/RequestHolder.java
new file mode 100644
index 0000000..fb8ec10
--- /dev/null
+++ b/frequency-control/src/main/java/com/abin/frequencycontrol/util/RequestHolder.java
@@ -0,0 +1,24 @@
+package com.abin.frequencycontrol.util;
+
+
+import com.abin.frequencycontrol.domain.dto.RequestInfo;
+
+/**
+ * 请求上下文
+ */
+public class RequestHolder {
+
+ private static final ThreadLocal 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();
+ }
+}
diff --git a/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/FrequencyControlConstant.java b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/FrequencyControlConstant.java
new file mode 100644
index 0000000..c07e5f4
--- /dev/null
+++ b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/FrequencyControlConstant.java
@@ -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";
+}
diff --git a/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/RedisUtils.java b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/RedisUtils.java
new file mode 100644
index 0000000..c1cc848
--- /dev/null
+++ b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/RedisUtils.java
@@ -0,0 +1,1127 @@
+package com.abin.mallchat.utils;
+
+import cn.hutool.extra.spring.SpringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.RedisConnectionUtils;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class RedisUtils {
+
+ private static StringRedisTemplate stringRedisTemplate;
+
+ static {
+ RedisUtils.stringRedisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
+ }
+
+ private static final String LUA_INCR_EXPIRE =
+ "local key,ttl=KEYS[1],ARGV[1] \n" +
+ " \n" +
+ "if redis.call('EXISTS',key)==0 then \n" +
+ " redis.call('SETEX',key,ttl,1) \n" +
+ " return 1 \n" +
+ "else \n" +
+ " return tonumber(redis.call('INCR',key)) \n" +
+ "end ";
+
+ public static Long inc(String key, int time, TimeUnit unit) {
+ RedisScript redisScript = new DefaultRedisScript<>(LUA_INCR_EXPIRE, Long.class);
+ return stringRedisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(unit.toSeconds(time)));
+ }
+
+ public static Long ZSetGet(String key) {
+ return stringRedisTemplate.opsForZSet().zCard(key);
+ }
+
+ public static void ZSetAddAndExpire(String key, long startTime, long expireTime, long currentTime) {
+ stringRedisTemplate.opsForZSet().add(key, String.valueOf(currentTime), currentTime);
+ // 删除周期之前的数据
+ stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, startTime);
+ // 过期时间窗口长度+时间间隔
+ stringRedisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * 指定缓存失效时间
+ *
+ * @param key 键
+ * @param time 时间(秒)
+ */
+ public static Boolean expire(String key, long time) {
+ try {
+ if (time > 0) {
+ stringRedisTemplate.expire(key, time, TimeUnit.SECONDS);
+ }
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 指定缓存失效时间
+ *
+ * @param key 键
+ * @param time 时间(秒)
+ * @param timeUnit 单位
+ */
+ public static Boolean expire(String key, long time, TimeUnit timeUnit) {
+ try {
+ if (time > 0) {
+ stringRedisTemplate.expire(key, time, timeUnit);
+ }
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 根据 key 获取过期时间
+ *
+ * @param key 键 不能为null
+ * @return 时间(秒) 返回0代表为永久有效
+ */
+ public static Long getExpire(String key) {
+ return stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 根据 key 获取过期时间
+ *
+ * @param key 键 不能为null
+ * @return 时间(秒) 返回0代表为永久有效
+ */
+ public static Long getExpire(String key, TimeUnit timeUnit) {
+ return stringRedisTemplate.getExpire(key, timeUnit);
+ }
+
+ /**
+ * 查找匹配key
+ *
+ * @param pattern key
+ * @return /
+ */
+ public static List scan(String pattern) {
+ ScanOptions options = ScanOptions.scanOptions().match(pattern).build();
+ RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory();
+ RedisConnection rc = Objects.requireNonNull(factory).getConnection();
+ Cursor cursor = rc.scan(options);
+ List result = new ArrayList<>();
+ while (cursor.hasNext()) {
+ result.add(new String(cursor.next()));
+ }
+ try {
+ RedisConnectionUtils.releaseConnection(rc, factory);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ return result;
+ }
+
+ /**
+ * 分页查询 key
+ *
+ * @param patternKey key
+ * @param page 页码
+ * @param size 每页数目
+ * @return /
+ */
+ public static List findKeysForPage(String patternKey, int page, int size) {
+ ScanOptions options = ScanOptions.scanOptions().match(patternKey).build();
+ RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory();
+ RedisConnection rc = Objects.requireNonNull(factory).getConnection();
+ Cursor cursor = rc.scan(options);
+ List result = new ArrayList<>(size);
+ int tmpIndex = 0;
+ int fromIndex = page * size;
+ int toIndex = page * size + size;
+ while (cursor.hasNext()) {
+ if (tmpIndex >= fromIndex && tmpIndex < toIndex) {
+ result.add(new String(cursor.next()));
+ tmpIndex++;
+ continue;
+ }
+ // 获取到满足条件的数据后,就可以退出了
+ if (tmpIndex >= toIndex) {
+ break;
+ }
+ tmpIndex++;
+ cursor.next();
+ }
+ try {
+ RedisConnectionUtils.releaseConnection(rc, factory);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ }
+ return result;
+ }
+
+ /**
+ * 判断key是否存在
+ *
+ * @param key 键
+ * @return true 存在 false不存在
+ */
+ public static Boolean hasKey(String key) {
+ try {
+ return stringRedisTemplate.hasKey(key);
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return false;
+ }
+ }
+
+
+ /**
+ * 删除缓存
+ *
+ * @param keys
+ */
+ public static void del(String... keys) {
+ if (keys != null && keys.length > 0) {
+ if (keys.length == 1) {
+ Boolean result = stringRedisTemplate.delete(keys[0]);
+ log.debug("--------------------------------------------");
+ log.debug("删除缓存:" + keys[0] + ",结果:" + result);
+ } else {
+ Set keySet = new HashSet<>();
+ for (String key : keys) {
+ Set stringSet = stringRedisTemplate.keys(key);
+ if (Objects.nonNull(stringSet) && !stringSet.isEmpty()) {
+ keySet.addAll(stringSet);
+ }
+ }
+ Long count = stringRedisTemplate.delete(keySet);
+ log.debug("--------------------------------------------");
+ log.debug("成功删除缓存:" + keySet);
+ log.debug("缓存删除数量:" + count + "个");
+ }
+ log.debug("--------------------------------------------");
+ }
+ }
+
+ public static void del(List keys) {
+ stringRedisTemplate.delete(keys);
+ }
+
+ // ============================String=============================
+
+ /**
+ * 普通缓存获取
+ *
+ * @param key 键
+ * @return 值
+ */
+ private static String get(String key) {
+ return key == null ? null : stringRedisTemplate.opsForValue().get(key);
+ }
+
+ /**
+ * 普通缓存放入
+ *
+ * @param key 键
+ * @param value 值
+ * @return true成功 false失败
+ */
+ public static Boolean set(String key, Object value) {
+ try {
+ stringRedisTemplate.opsForValue().set(key, objToStr(value));
+ return true;
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return false;
+ }
+ }
+
+ public static String getStr(String key) {
+ return get(key, String.class);
+ }
+
+ public static T get(String key, Class tClass) {
+ String s = get(key);
+ return toBeanOrNull(s, tClass);
+ }
+
+ public static List mget(Collection keys, Class tClass) {
+ List list = stringRedisTemplate.opsForValue().multiGet(keys);
+ if (Objects.isNull(list)) {
+ return new ArrayList<>();
+ }
+ return list.stream().map(o -> toBeanOrNull(o, tClass)).collect(Collectors.toList());
+ }
+
+ static T toBeanOrNull(String json, Class tClass) {
+ return json == null ? null : JsonUtils.toObj(json, tClass);
+ }
+
+ public static String objToStr(Object o) {
+ return JsonUtils.toStr(o);
+ }
+
+ public static void mset(Map map, long time) {
+ Map collect = map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, (e) -> objToStr(e.getValue())));
+ stringRedisTemplate.opsForValue().multiSet(collect);
+ map.forEach((key, value) -> {
+ expire(key, time);
+ });
+ }
+
+
+ /**
+ * 普通缓存放入并设置时间
+ *
+ * @param key 键
+ * @param value 值
+ * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
+ * @return true成功 false 失败
+ */
+ public static Boolean set(String key, Object value, long time) {
+ try {
+ if (time > 0) {
+ stringRedisTemplate.opsForValue().set(key, objToStr(value), time, TimeUnit.SECONDS);
+ } else {
+ set(key, value);
+ }
+ return true;
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 普通缓存放入并设置时间
+ *
+ * @param key 键
+ * @param value 值
+ * @param time 时间
+ * @param timeUnit 类型
+ * @return true成功 false 失败
+ */
+ public static Boolean set(String key, Object value, long time, TimeUnit timeUnit) {
+ try {
+ if (time > 0) {
+ stringRedisTemplate.opsForValue().set(key, objToStr(value), time, timeUnit);
+ } else {
+ set(key, value);
+ }
+ return true;
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ return false;
+ }
+ }
+
+ // ================================Map=================================
+
+ /**
+ * HashGet
+ *
+ * @param key 键 不能为null
+ * @param item 项 不能为null
+ * @return 值
+ */
+ public static Object hget(String key, String item) {
+ return stringRedisTemplate.opsForHash().get(key, item);
+ }
+
+ /**
+ * 获取hashKey对应的所有键值
+ *
+ * @param key 键
+ * @return 对应的多个键值
+ */
+ public static Map