From a13ec103c5cce2742327280fdbd68080813459bd Mon Sep 17 00:00:00 2001 From: brian Date: Fri, 21 Jul 2023 11:22:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=99=BA=E8=83=BD=E5=88=86=E6=9E=90?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E8=BF=9B=E8=A1=8C=E5=88=86=E5=B8=83=E5=BC=8F?= =?UTF-8?q?=E9=99=90=E6=B5=81(=E5=9F=BA=E4=BA=8E=20RedissonRateLimiter)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 +- ...eLimiterTag.java => GuavaRateLimiter.java} | 4 +- .../annotation/RedissonRateLimiter.java | 30 +++++++++ ....java => GuavaRateLimiterInterceptor.java} | 16 ++--- .../aop/RedissonRateLimiterInterceptor.java | 65 +++++++++++++++++++ .../peng/answerbi/config/RedissonConfig.java | 45 +++++++++++++ .../answerbi/controller/ChartController.java | 13 +--- .../answerbi/manager/RedisLimiterManager.java | 44 +++++++++++++ src/main/resources/application.yml | 13 ++-- 9 files changed, 209 insertions(+), 28 deletions(-) rename src/main/java/top/peng/answerbi/annotation/{RateLimiterTag.java => GuavaRateLimiter.java} (86%) create mode 100644 src/main/java/top/peng/answerbi/annotation/RedissonRateLimiter.java rename src/main/java/top/peng/answerbi/aop/{RateLimiterInterceptor.java => GuavaRateLimiterInterceptor.java} (80%) create mode 100644 src/main/java/top/peng/answerbi/aop/RedissonRateLimiterInterceptor.java create mode 100644 src/main/java/top/peng/answerbi/config/RedissonConfig.java create mode 100644 src/main/java/top/peng/answerbi/manager/RedisLimiterManager.java diff --git a/pom.xml b/pom.xml index 28c7775..9899a9c 100644 --- a/pom.xml +++ b/pom.xml @@ -129,7 +129,12 @@ com.google.guava guava - 29.0-jre + 31.1-jre + + + org.redisson + redisson + 3.21.3 diff --git a/src/main/java/top/peng/answerbi/annotation/RateLimiterTag.java b/src/main/java/top/peng/answerbi/annotation/GuavaRateLimiter.java similarity index 86% rename from src/main/java/top/peng/answerbi/annotation/RateLimiterTag.java rename to src/main/java/top/peng/answerbi/annotation/GuavaRateLimiter.java index 4ac6d62..1599f4d 100644 --- a/src/main/java/top/peng/answerbi/annotation/RateLimiterTag.java +++ b/src/main/java/top/peng/answerbi/annotation/GuavaRateLimiter.java @@ -13,7 +13,7 @@ import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; /** - * RateLimiter 限流注解 + * GuavaRateLimiter 单机限流注解 与 RedissonRateLimiter 二选一使用即可 * * @author yunpeng * @version 1.0 2023/7/20 @@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented -public @interface RateLimiterTag { +public @interface GuavaRateLimiter { int NOT_LIMITED = 0; /** * 用户qps, 每个用户每秒的请求限制 diff --git a/src/main/java/top/peng/answerbi/annotation/RedissonRateLimiter.java b/src/main/java/top/peng/answerbi/annotation/RedissonRateLimiter.java new file mode 100644 index 0000000..320ef62 --- /dev/null +++ b/src/main/java/top/peng/answerbi/annotation/RedissonRateLimiter.java @@ -0,0 +1,30 @@ +/* + * @(#)RateLimiter.java + * + * Copyright © 2023 YunPeng Corporation. + */ +package top.peng.answerbi.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * RedissonRateLimiter 分布式限流注解 与 GuavaRateLimiter 二选一使用即可 + * + * @author yunpeng + * @version 1.0 2023/7/20 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RedissonRateLimiter { + int NOT_LIMITED = 0; + /** + * 用户qps, 每个用户每秒的请求限制 + */ + long qps() default NOT_LIMITED; +} diff --git a/src/main/java/top/peng/answerbi/aop/RateLimiterInterceptor.java b/src/main/java/top/peng/answerbi/aop/GuavaRateLimiterInterceptor.java similarity index 80% rename from src/main/java/top/peng/answerbi/aop/RateLimiterInterceptor.java rename to src/main/java/top/peng/answerbi/aop/GuavaRateLimiterInterceptor.java index c52e96a..8960afa 100644 --- a/src/main/java/top/peng/answerbi/aop/RateLimiterInterceptor.java +++ b/src/main/java/top/peng/answerbi/aop/GuavaRateLimiterInterceptor.java @@ -20,14 +20,14 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import top.peng.answerbi.annotation.RateLimiterTag; +import top.peng.answerbi.annotation.GuavaRateLimiter; import top.peng.answerbi.common.ErrorCode; import top.peng.answerbi.exception.ThrowUtils; import top.peng.answerbi.model.entity.User; import top.peng.answerbi.service.UserService; /** - * RateLimiterInterceptor 限流切面 + * GuavaRateLimiterInterceptor 单机限流切面 * * @author yunpeng * @version 1.0 2023/7/20 @@ -35,14 +35,14 @@ import top.peng.answerbi.service.UserService; @Slf4j @Aspect @Component -public class RateLimiterInterceptor { +public class GuavaRateLimiterInterceptor { @Resource private UserService userService; private static final ConcurrentMap RATE_LIMITER_CACHE = new ConcurrentHashMap<>(); - @Around("@annotation(rateLimiterTag)") - public Object doInterceptor(ProceedingJoinPoint point, RateLimiterTag rateLimiterTag) throws Throwable { + @Around("@annotation(guavaRateLimiter)") + public Object doInterceptor(ProceedingJoinPoint point, GuavaRateLimiter guavaRateLimiter) throws Throwable { RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); @@ -51,8 +51,8 @@ public class RateLimiterInterceptor { // 当前请求方法 Method method = ((MethodSignature) point.getSignature()).getMethod(); String key = loginUser.getId() + "#" + method.getName(); - if (rateLimiterTag != null && rateLimiterTag.qps() > RateLimiterTag.NOT_LIMITED) { - double qps = rateLimiterTag.qps(); + if (guavaRateLimiter != null && guavaRateLimiter.qps() > GuavaRateLimiter.NOT_LIMITED) { + double qps = guavaRateLimiter.qps(); if (RATE_LIMITER_CACHE.get(key) == null) { // 初始化 QPS @@ -64,7 +64,7 @@ public class RateLimiterInterceptor { if (RATE_LIMITER_CACHE.get(key) != null){ RateLimiter limiter = RATE_LIMITER_CACHE.get(key); ThrowUtils.throwIf( - !limiter.tryAcquire(rateLimiterTag.timeout(), rateLimiterTag.timeUnit()), + !limiter.tryAcquire(guavaRateLimiter.timeout(), guavaRateLimiter.timeUnit()), ErrorCode.TOO_MANY_REQUEST); } } diff --git a/src/main/java/top/peng/answerbi/aop/RedissonRateLimiterInterceptor.java b/src/main/java/top/peng/answerbi/aop/RedissonRateLimiterInterceptor.java new file mode 100644 index 0000000..0d9497a --- /dev/null +++ b/src/main/java/top/peng/answerbi/aop/RedissonRateLimiterInterceptor.java @@ -0,0 +1,65 @@ +/* + * @(#)RateLimiterInterceptor.java + * + * Copyright © 2023 YunPeng Corporation. + */ +package top.peng.answerbi.aop; + +import com.google.common.util.concurrent.RateLimiter; +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +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 org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import top.peng.answerbi.annotation.GuavaRateLimiter; +import top.peng.answerbi.annotation.RedissonRateLimiter; +import top.peng.answerbi.common.ErrorCode; +import top.peng.answerbi.exception.ThrowUtils; +import top.peng.answerbi.manager.RedisLimiterManager; +import top.peng.answerbi.model.entity.User; +import top.peng.answerbi.service.UserService; + +/** + * RedissonRateLimiterInterceptor 分布式限流切面 + * + * @author yunpeng + * @version 1.0 2023/7/20 + */ +@Slf4j +@Aspect +@Component +public class RedissonRateLimiterInterceptor { + @Resource + private UserService userService; + + @Resource + private RedisLimiterManager redisLimiterManager; + + private static final ConcurrentMap RATE_LIMITER_CACHE = new ConcurrentHashMap<>(); + + @Around("@annotation(rRateLimiter)") + public Object doInterceptor(ProceedingJoinPoint point, RedissonRateLimiter rRateLimiter) throws Throwable { + + RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); + HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + // 当前登录用户 + User loginUser = userService.getLoginUser(request); + // 当前请求方法 + Method method = ((MethodSignature) point.getSignature()).getMethod(); + String key = loginUser.getId() + "#" + method.getName(); + if (rRateLimiter != null && rRateLimiter.qps() > RedissonRateLimiter.NOT_LIMITED) { + redisLimiterManager.doRateLimit(key, rRateLimiter.qps()); + } + return point.proceed(); + } + +} diff --git a/src/main/java/top/peng/answerbi/config/RedissonConfig.java b/src/main/java/top/peng/answerbi/config/RedissonConfig.java new file mode 100644 index 0000000..bd4156e --- /dev/null +++ b/src/main/java/top/peng/answerbi/config/RedissonConfig.java @@ -0,0 +1,45 @@ +/* + * @(#)RedissonConfig.java + * + * Copyright © 2023 YunPeng Corporation. + */ +package top.peng.answerbi.config; + +import lombok.Data; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * RedissonConfig + * + * @author yunpeng + * @version 1.0 2023/7/20 + */ +@Configuration +@ConfigurationProperties(prefix = "spring.redis") +@Data +public class RedissonConfig { + + private Integer database; + + private String host; + + private Integer port; + + private String password; + + @Bean + public RedissonClient getRedissonClient(){ + Config config = new Config(); + //添加单机redisson配置 + config.useSingleServer() + .setDatabase(database) + .setAddress("redis://" + host + ":" + port) + .setPassword(password); + return Redisson.create(config); + } +} diff --git a/src/main/java/top/peng/answerbi/controller/ChartController.java b/src/main/java/top/peng/answerbi/controller/ChartController.java index aceef12..8c54dfb 100644 --- a/src/main/java/top/peng/answerbi/controller/ChartController.java +++ b/src/main/java/top/peng/answerbi/controller/ChartController.java @@ -2,22 +2,18 @@ package top.peng.answerbi.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.google.gson.Gson; -import java.io.File; import java.util.Arrays; -import java.util.List; -import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.poi.util.StringUtil; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; import top.peng.answerbi.annotation.AuthCheck; -import top.peng.answerbi.annotation.RateLimiterTag; +import top.peng.answerbi.annotation.GuavaRateLimiter; +import top.peng.answerbi.annotation.RedissonRateLimiter; import top.peng.answerbi.common.CommonResponse; import top.peng.answerbi.common.DeleteRequest; import top.peng.answerbi.common.ErrorCode; import top.peng.answerbi.common.ResultUtils; import top.peng.answerbi.constant.BiConstant; -import top.peng.answerbi.constant.FileConstant; import top.peng.answerbi.constant.UserConstant; import top.peng.answerbi.exception.BusinessException; import top.peng.answerbi.exception.ThrowUtils; @@ -27,10 +23,8 @@ import top.peng.answerbi.model.dto.chart.ChartEditRequest; import top.peng.answerbi.model.dto.chart.ChartQueryRequest; import top.peng.answerbi.model.dto.chart.ChartUpdateRequest; import top.peng.answerbi.model.dto.chart.GenChartByAiRequest; -import top.peng.answerbi.model.dto.file.UploadFileRequest; import top.peng.answerbi.model.entity.Chart; import top.peng.answerbi.model.entity.User; -import top.peng.answerbi.model.enums.FileUploadBizEnum; import top.peng.answerbi.model.vo.BiResponse; import top.peng.answerbi.service.ChartService; import top.peng.answerbi.service.UserService; @@ -238,7 +232,7 @@ public class ChartController { * @return */ @PostMapping("/gen") - @RateLimiterTag(qps = 1.0, timeout = 100) + @RedissonRateLimiter(qps = 1) public CommonResponse genChartByAi(@RequestPart("file") MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) { String chartName = genChartByAiRequest.getChartName(); @@ -269,7 +263,6 @@ public class ChartController { //压缩后的数据 String csvData = ExcelUtils.excelToCsv(multipartFile); userInput.append(csvData).append("\n"); - String aiResult = aiManager.doChat(BiConstant.BI_MODEL_ID, userInput.toString()); BiResponse biResponse = aiManager.aiAnsToBiResp(aiResult); diff --git a/src/main/java/top/peng/answerbi/manager/RedisLimiterManager.java b/src/main/java/top/peng/answerbi/manager/RedisLimiterManager.java new file mode 100644 index 0000000..545d4c9 --- /dev/null +++ b/src/main/java/top/peng/answerbi/manager/RedisLimiterManager.java @@ -0,0 +1,44 @@ +/* + * @(#)RedisLimiterManager.java + * + * Copyright © 2023 YunPeng Corporation. + */ +package top.peng.answerbi.manager; + +import javax.annotation.Resource; +import org.redisson.api.RRateLimiter; +import org.redisson.api.RateIntervalUnit; +import org.redisson.api.RateType; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Service; +import top.peng.answerbi.common.ErrorCode; +import top.peng.answerbi.config.RedissonConfig; +import top.peng.answerbi.exception.ThrowUtils; + +/** + * RedisLimiterManager 提供 RedisLimiter 限流基础服务的 + * + * @author yunpeng + * @version 1.0 2023/7/21 + */ +@Service +public class RedisLimiterManager { + @Resource + private RedissonClient redissonClient; + + /** + * 限流操作 + * @param key 区分不同限流器,比如不同的用户应该分别统计 + */ + public void doRateLimit(String key, long qps){ + //创建一个每秒最低访问两次的限流器 key为限流器名称 + RRateLimiter rRateLimiter = redissonClient.getRateLimiter(key); + //限流器的规则(每秒 qps 个请求(1秒中生产qps个令牌); 连续的请求,最多只能有1个请求被允许通过) + //RateType.OVERALL 表示速率限制作用于整个令牌桶, 即所有实例共享 + rRateLimiter.trySetRate(RateType.OVERALL, qps, 1, RateIntervalUnit.SECONDS); + //请求一个令牌 + boolean canOp = rRateLimiter.tryAcquire(1); + //没有令牌可用,抛异常 + ThrowUtils.throwIf(!canOp, ErrorCode.TOO_MANY_REQUEST); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6c32cd0..c542d45 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,13 +22,12 @@ spring: username: root password: 123456 # Redis 配置 - # todo 需替换配置,然后取消注释 -# redis: -# database: 1 -# host: localhost -# port: 6379 -# timeout: 5000 -# password: 123456 + redis: + database: 1 + host: localhost + port: 6379 + timeout: 5000 + password: 123456 # Elasticsearch 配置 # todo 需替换配置,然后取消注释 # elasticsearch: