【新增】RateLimiter 限流器,支持全局、用户、IP 等级别的限流

This commit is contained in:
YunaiV 2024-04-11 22:43:37 +08:00
parent 2e03dcba69
commit cc50891632
15 changed files with 440 additions and 8 deletions

View File

@ -207,9 +207,7 @@
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 | | 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 | | 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 | | 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 | | 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 |
| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 |
| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 |
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 | | 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 | | 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
@ -304,7 +302,6 @@
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 7.0.0 | [文档](https://doc.iocoder.cn/bpm/) | | [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 7.0.0 | [文档](https://doc.iocoder.cn/bpm/) |
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) | | [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
| [Springdoc](https://springdoc.org/) | Swagger 文档 | 2.2.0 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) | | [Springdoc](https://springdoc.org/) | Swagger 文档 | 2.2.0 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 2.1.0 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 9.0.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) | | [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 9.0.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 3.1.8 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) | | [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 3.1.8 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.15.3 | | | [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.15.3 | |

View File

@ -20,7 +20,7 @@
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId> <artifactId>yudao-spring-boot-starter-web</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 OncePerRequestFilter 使用到 --> <scope>provided</scope> <!-- 设置为 provided只有限流、幂等使用到 -->
</dependency> </dependency>
<!-- DB 相关 --> <!-- DB 相关 -->

View File

@ -37,7 +37,7 @@ public class IdempotentAspect {
} }
@Around(value = "@annotation(idempotent)") @Around(value = "@annotation(idempotent)")
public Object beforePointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { public Object aroundPointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 获得 IdempotentKeyResolver // 获得 IdempotentKeyResolver
IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
@ -48,7 +48,7 @@ public class IdempotentAspect {
boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
// 锁定失败抛出异常 // 锁定失败抛出异常
if (!success) { if (!success) {
log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); log.info("[aroundPointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
} }

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.framework.ratelimiter.config;
import cn.iocoder.yudao.framework.ratelimiter.core.aop.RateLimiterAspect;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.*;
import cn.iocoder.yudao.framework.ratelimiter.core.redis.RateLimiterRedisDAO;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import java.util.List;
@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
public class YudaoRateLimiterConfiguration {
@Bean
public RateLimiterAspect rateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
return new RateLimiterAspect(keyResolvers, rateLimiterRedisDAO);
}
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public RateLimiterRedisDAO rateLimiterRedisDAO(RedissonClient redissonClient) {
return new RateLimiterRedisDAO(redissonClient);
}
// ========== 各种 RateLimiterRedisDAO Bean ==========
@Bean
public DefaultRateLimiterKeyResolver defaultRateLimiterKeyResolver() {
return new DefaultRateLimiterKeyResolver();
}
@Bean
public UserRateLimiterKeyResolver userRateLimiterKeyResolver() {
return new UserRateLimiterKeyResolver();
}
@Bean
public ClientIpRateLimiterKeyResolver clientIpRateLimiterKeyResolver() {
return new ClientIpRateLimiterKeyResolver();
}
@Bean
public ServerNodeRateLimiterKeyResolver serverNodeRateLimiterKeyResolver() {
return new ServerNodeRateLimiterKeyResolver();
}
@Bean
public ExpressionRateLimiterKeyResolver expressionRateLimiterKeyResolver() {
return new ExpressionRateLimiterKeyResolver();
}
}

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.framework.ratelimiter.core.annotation;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.ClientIpRateLimiterKeyResolver;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.DefaultRateLimiterKeyResolver;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.ServerNodeRateLimiterKeyResolver;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl.UserRateLimiterKeyResolver;
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;
/**
* 限流注解
*
* @author 芋道源码
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {
/**
* 限流的时间默认为 1
*/
int time() default 1;
/**
* 时间单位默认为 SECONDS
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 限流次数
*/
int count() default 100;
/**
* 提示信息请求过快的提示
*
* @see GlobalErrorCodeConstants#TOO_MANY_REQUESTS
*/
String message() default ""; // 为空时使用 TOO_MANY_REQUESTS 错误提示
/**
* 使用的 Key 解析器
*
* @see DefaultRateLimiterKeyResolver 全局级别
* @see UserRateLimiterKeyResolver 用户 ID 级别
* @see ClientIpRateLimiterKeyResolver 用户 IP 级别
* @see ServerNodeRateLimiterKeyResolver 服务器 Node 级别
* @see ExpressionIdempotentKeyResolver 自定义表达式通过 {@link #keyArg()} 计算
*/
Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;
/**
* 使用的 Key 参数
*/
String keyArg() default "";
}

View File

@ -0,0 +1,60 @@
package cn.iocoder.yudao.framework.ratelimiter.core.aop;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import cn.iocoder.yudao.framework.ratelimiter.core.redis.RateLimiterRedisDAO;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Map;
/**
* 拦截声明了 {@link RateLimiter} 注解的方法实现限流操作
*
* @author 芋道源码
*/
@Aspect
@Slf4j
public class RateLimiterAspect {
/**
* RateLimiterKeyResolver 集合
*/
private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;
private final RateLimiterRedisDAO rateLimiterRedisDAO;
public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
this.rateLimiterRedisDAO = rateLimiterRedisDAO;
}
@Before("@annotation(rateLimiter)")
public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 获得 IdempotentKeyResolver 对象
RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
// 解析 Key
String key = keyResolver.resolver(joinPoint, rateLimiter);
// 获取 1 次限流
boolean success = rateLimiterRedisDAO.tryAcquire(key,
rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit());
if (!success) {
log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs());
String message = StrUtil.blankToDefault(rateLimiter.message(),
GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());
throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message);
}
}
}

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import org.aspectj.lang.JoinPoint;
/**
* 限流 Key 解析器接口
*
* @author 芋道源码
*/
public interface RateLimiterKeyResolver {
/**
* 解析一个 Key
*
* @param rateLimiter 限流注解
* @param joinPoint AOP 切面
* @return Key
*/
String resolver(JoinPoint joinPoint, RateLimiter rateLimiter);
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
* IP 级别的限流 Key 解析器使用方法名 + 方法参数 + IP组装成一个 Key
*
* 为了避免 Key 过长使用 MD5 进行压缩
*
* @author 芋道源码
*/
public class ClientIpRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
String clientIp = ServletUtils.getClientIP();
return SecureUtil.md5(methodName + argsStr + clientIp);
}
}

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
* 默认全局级别限流 Key 解析器使用方法名 + 方法参数组装成一个 Key
*
* 为了避免 Key 过长使用 MD5 进行压缩
*
* @author 芋道源码
*/
public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
return SecureUtil.md5(methodName + argsStr);
}
}

View File

@ -0,0 +1,64 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
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;
/**
* 基于 Spring EL 表达式的 {@link RateLimiterKeyResolver} 实现类
*
* @author 芋道源码
*/
public class ExpressionRateLimiterKeyResolver implements RateLimiterKeyResolver {
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final ExpressionParser expressionParser = new SpelExpressionParser();
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
// 获得被拦截方法参数名列表
Method method = getMethod(joinPoint);
Object[] args = joinPoint.getArgs();
String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
// 准备 Spring EL 表达式解析的上下文
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
if (ArrayUtil.isNotEmpty(parameterNames)) {
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
}
// 解析参数
Expression expression = expressionParser.parseExpression(rateLimiter.keyArg());
return expression.getValue(evaluationContext, String.class);
}
private static Method getMethod(JoinPoint point) {
// 处理声明在类上的情况
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
if (!method.getDeclaringClass().isInterface()) {
return method;
}
// 处理声明在接口上的情况
try {
return point.getTarget().getClass().getDeclaredMethod(
point.getSignature().getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,27 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.system.SystemUtil;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import org.aspectj.lang.JoinPoint;
/**
* Server 节点级别的限流 Key 解析器使用方法名 + 方法参数 + IP组装成一个 Key
*
* 为了避免 Key 过长使用 MD5 进行压缩
*
* @author 芋道源码
*/
public class ServerNodeRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
String serverNode = String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
return SecureUtil.md5(methodName + argsStr + serverNode);
}
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.iocoder.yudao.framework.ratelimiter.core.annotation.RateLimiter;
import cn.iocoder.yudao.framework.ratelimiter.core.keyresolver.RateLimiterKeyResolver;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import org.aspectj.lang.JoinPoint;
/**
* 用户级别的限流 Key 解析器使用方法名 + 方法参数 + userId + userType组装成一个 Key
*
* 为了避免 Key 过长使用 MD5 进行压缩
*
* @author 芋道源码
*/
public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {
@Override
public String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {
String methodName = joinPoint.getSignature().toString();
String argsStr = StrUtil.join(",", joinPoint.getArgs());
Long userId = WebFrameworkUtils.getLoginUserId();
Integer userType = WebFrameworkUtils.getLoginUserType();
return SecureUtil.md5(methodName + argsStr + userId + userType);
}
}

View File

@ -0,0 +1,60 @@
package cn.iocoder.yudao.framework.ratelimiter.core.redis;
import lombok.AllArgsConstructor;
import org.redisson.api.*;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 限流 Redis DAO
*
* @author 芋道源码
*/
@AllArgsConstructor
public class RateLimiterRedisDAO {
/**
* 限流操作
*
* KEY 格式rate_limiter:%s // 参数为 uuid
* VALUE 格式String
* 过期时间不固定
*/
private static final String RATE_LIMITER = "rate_limiter:%s";
private final RedissonClient redissonClient;
public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
// 1. 获得 RRateLimiter并设置 rate 速率
RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
// 2. 尝试获取 1
return rateLimiter.tryAcquire();
}
private static String formatKey(String key) {
return String.format(RATE_LIMITER, key);
}
private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
String redisKey = formatKey(key);
RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
long rateInterval = timeUnit.toSeconds(time);
// 1. 如果不存在设置 rate 速率
RateLimiterConfig config = rateLimiter.getConfig();
if (config == null) {
rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
return rateLimiter;
}
// 2. 如果存在并且配置相同则直接返回
if (config.getRateType() == RateType.OVERALL
&& Objects.equals(config.getRate(), count)
&& Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
return rateLimiter;
}
// 3. 如果存在并且配置不同则进行新建
rateLimiter.setRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);
return rateLimiter;
}
}

View File

@ -0,0 +1,4 @@
/**
* 限流组件基于 Redisson {@link org.redisson.api.RRateLimiter} 限流实现
*/
package cn.iocoder.yudao.framework.ratelimiter;

View File

@ -1,2 +1,3 @@
cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration
cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration
cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration