对智能分析请求进行分布式限流(基于 RedissonRateLimiter)
This commit is contained in:
parent
89ee9969a2
commit
a13ec103c5
7
pom.xml
7
pom.xml
@ -129,7 +129,12 @@
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>29.0-jre</version>
|
||||
<version>31.1-jre</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson</artifactId>
|
||||
<version>3.21.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@ -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, 每个用户每秒的请求限制
|
||||
@ -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;
|
||||
}
|
||||
@ -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<String, RateLimiter> 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);
|
||||
}
|
||||
}
|
||||
@ -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<String, RateLimiter> 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();
|
||||
}
|
||||
|
||||
}
|
||||
45
src/main/java/top/peng/answerbi/config/RedissonConfig.java
Normal file
45
src/main/java/top/peng/answerbi/config/RedissonConfig.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<BiResponse> 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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user