Java Redisson多策略注解限流实现
在Web开发中,为防止恶意刷量或突发流量压垮服务,合理的限流机制至关重要。本方案基于Java生态,利用Spring AOP拦截带有自定义注解的请求,结合Redisson分布式锁库中的RateLimiter组件,实现灵活的多维度限流控制。
1. 设计思路与目标
限流的核心在于识别“谁”在请求以及“请求什么”。传统的硬编码方式难以应对不断变化的业务场景(如按IP限流、按用户限流、按接口限流等)。本方案通过以下特性实现解耦与扩展:
- 注解驱动:开发者只需在Controller或Service方法上添加注解,即可启用限流。
- 多策略支持:通过枚举定义多种限流维度(IP、用户、URI等),并通过工厂模式动态生成唯一键值(Key)。
- Token Bucket算法:使用Redisson的RRateLimiter,基于漏桶/令牌桶原理,允许瞬时突发流量并平滑处理。
2. 核心组件实现
第1步:定义限流注解
创建自定义注解 @RedisLimit,用于标记需要限流的方法。注解包含前缀、自定义Key、时间窗口(秒)、许可数量以及限流类型。
// 限流注解定义
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimit {
String prefix() default "rateLimit:"; // Redis Key前缀
String key() default ""; // 自定义Key,优先级最高
int time() default 1; // 时间窗口(秒)
int count(); // 时间窗口内的最大请求数
LimitType type() default LimitType.CUSTOM; // 限流策略类型
}
第2步:定义限流类型枚举
枚举 LimitType 定义了支持的限流维度,涵盖了从单一用户到复合条件(如URI+参数+用户ID)的各种场景。
public enum LimitType {
CUSTOM, // 使用注解中自定义的key
IP, // 基于请求者IP
METHOD, // 基于类名+方法名
PARAMS, // 基于类名+方法名+参数
USER, // 基于类名+方法名+参数+用户ID
REQUEST_URI, // 基于Request URI
REQUESTURI_USERID, // 基于URI+用户ID
SINGLEUSER, // 仅基于用户ID
SINGLEMETHOD, // 仅基于方法签名
REQUEST_URI_PARAMS, // 基于URI+参数
REQUEST_URI_PARAMS_USERID // 基于URI+参数+用户ID
}
第3步:构建Key生成策略工具类
这是方案的核心部分。为了避免大量的 if-else 判断,我们使用 Map<LimitType, Function> 映射表,配合函数式接口动态组装Key。其中 BaseContext 为获取当前上下文用户ID的工具类。
@Component
public class ProceedingJoinPointUtil {
@Autowired
private HttpServletRequest request;
private Map<LimitType, Function<ProceedingJoinPoint, String>> functionMap = new HashMap<>(9);
@PostConstruct
void initMap() {
// 初始化各种策略对应的Key生成逻辑
functionMap.put(LimitType.METHOD, this::getMethodTypeKey);
functionMap.put(LimitType.PARAMS, this::getParamsTypeKey);
functionMap.put(LimitType.USER, this::getUserTypeKey);
functionMap.put(LimitType.REQUEST_URI, jp -> request.getRequestURI());
functionMap.put(LimitType.REQUESTURI_USERID, jp -> request.getRequestURI() + BaseContext.getUserId());
functionMap.put(LimitType.REQUEST_URI_PARAMS, jp -> request.getRequestURI() + getParams(jp));
functionMap.put(LimitType.REQUEST_URI_PARAMS_USERID, jp -> request.getRequestURI() + getParams(jp) + BaseContext.getUserId());
functionMap.put(LimitType.SINGLEUSER, jp -> String.valueOf(BaseContext.getUserId()));
functionMap.put(LimitType.SINGLEMETHOD, jp -> {
StringBuilder sb = new StringBuilder();
appendMthodName(jp, sb);
return sb.toString();
});
}
/**
* 根据注解类型获取最终Key
*/
public String getKey(ProceedingJoinPoint joinPoint, RedisLimit redisLimit) {
if (redisLimit.type() != LimitType.CUSTOM) {
Function<ProceedingJoinPoint, String> func = functionMap.get(redisLimit.type());
return redisLimit.prefix() + func.apply(joinPoint);
} else {
// 自定义Key直接使用前缀拼接
return redisLimit.prefix() + redisLimit.key();
}
}
private void appendMthodName(ProceedingJoinPoint joinPoint, StringBuilder sb) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
sb.append(joinPoint.getTarget().getClass().getName()).append(method.getName());
}
private void appendParams(ProceedingJoinPoint joinPoint, StringBuilder sb) {
for (Object arg : joinPoint.getArgs()) {
if (!(arg instanceof MultipartFile)) { // 忽略文件上传参数以防内存溢出
sb.append(arg.toString());
}
}
}
private String getParams(ProceedingJoinPoint joinPoint) {
StringBuilder sb = new StringBuilder();
appendParams(joinPoint, sb);
return sb.toString();
}
}
第4步:编写AOP切面逻辑
利用Spring AOP的 @Around 通知拦截注解,在执行方法前后进行限流检查。使用 trySetRate 设置速率,并通过 tryAcquire 尝试获取令牌。
@Aspect
@Component
@Slf4j
public class RedisLimitAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
private ProceedingJoinPointUtil joinPointUtil;
@Pointcut("@annotation(com.yourpackage.RedisLimit)")
private void pointCut() {}
@Around("pointCut() && @annotation(redisLimit)")
public Object around(ProceedingJoinPoint joinPoint, RedisLimit redisLimit) throws Throwable {
// 1. 生成唯一限流Key
String key = joinPointUtil.getKey(joinPoint, redisLimit);
// 2. 获取Redisson限流器
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// 3. 设置速率:OVERALL模式下,总共有count个令牌,每time秒补充一次
rateLimiter.trySetRate(RateType.OVERALL, redisLimit.count(), redisLimit.time(), RateIntervalUnit.SECONDS);
// 4. 尝试获取令牌(非阻塞)
boolean canExecute = rateLimiter.tryAcquire();
if (!canExecute) {
// 限流触发,返回错误信息
return ResultData.failed("访问过于频繁,请稍后再试");
}
// 5. 执行原始方法
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
3. 使用示例与完成判断
在实际Controller中,只需添加注解即可完成限流配置。
@RestController
public class UserController {
/**
* 示例1:按用户ID限流,每秒最多10次
*/
@RedisLimit(type = LimitType.USER, count = 10, time = 1)
@GetMapping("/user/info")
public User getUserInfo() {
return userService.getInfo();
}
/**
* 示例2:按接口本身限流,每分钟最多100次
*/
@RedisLimit(type = LimitType.METHOD, count = 100, time = 60)
@PostMapping("/api/sale")
public Order createOrder() {
return orderService.create();
}
}
完成判断:当请求频率超过设定阈值时,API将立即返回预设的错误提示(如“访问过于频繁”),且不会执行底层业务逻辑。可通过监控Redis中对应的Key TTL及剩余令牌数来验证限流生效情况。
4. 注意事项
- 性能考量:每次请求都会向Redis发送命令(get/set),在高并发场景下需确保Redis实例的网络和IO瓶颈不是系统短板。对于极高吞吐场景,可考虑本地缓存+分布式限流的混合方案。
- 文件上传处理:在生成Key时已排除
MultipartFile对象,避免因为大型文件导致的String拼接开销或空指针异常。 - Key冲突:自定义Key时务必保证唯一性,否则可能导致不同接口的限流互相影响。
以上就是Java Redisson多策略注解限流的详细内容,更多关于Java Redisson限流技巧的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













