Springboot(五十八)SpringBoot3使用Redisson实现接口的限流功能

这部分我记录一下我使用redission实现接口限流的全过程。

关于自定义注解,请移步《SpringBoot(二十六)SpringBoot自定义注解

一:redission自定义限流注解主要流程

对接口实现限流,主要使用了Redisson提供的限流API方法;使用很简单:

第一步:声明一个限流器;

java 复制代码
 RRateLimiter rRateLimiter = redissonClient.getRateLimiter(rateLimiterKey);

第二步:设置速率;举例:5秒中产生2个令牌

java 复制代码
rRateLimiter.trySetRate(RateType.OVERALL, 2, 5, RateIntervalUnit.SECONDS);

第三步:试图获取一个令牌,获取到,返回true;否则,返回false

javascript 复制代码
rateLimiter.tryAcquire();

二:自定义限流注解接口

MyRateLimiter.java

java 复制代码
package com.modules.customannotations.myAnnotation;
 
import com.modules.customannotations.enums.LimitType;
 
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 MyRateLimiter {
    /**
     * 限流key,支持使用Spring el表达式来动态获取方法上的参数值
     */
    String rateKey() default "";
 
    /**
     * 限流时间,单位秒
     */
    int time() default 60;
 
    /**
     * 限流次数
     */
    int count() default 100;
 
    /**
     * 限流类型
     */
    LimitType limitType() default LimitType.DEFAULT;
 
    /**
     * 提示消息
     */
    String errMsg() default "接口请求过于频繁,请稍后再试!";
}

三:实现限流注解接口

MyRateLimiterAspect.java

java 复制代码
package com.modules.customannotations.annotationAspect;
 
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.modules.customannotations.enums.LimitType;
import com.modules.customannotations.myAnnotation.MyRateLimiter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
 
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
 
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class MyRateLimiterAspect {
 
    private final RedissonClient redissonClient;
 
    /**
     * 定义spel表达式解析器
     */
    private final ExpressionParser parser = new SpelExpressionParser();
    /**
     * 定义spel解析模版
     */
    private final ParserContext parserContext = new TemplateParserContext();
    /**
     * 定义spel上下文对象进行解析
     */
    private final EvaluationContext context = new StandardEvaluationContext();
    /**
     * 方法参数解析器
     */
    private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
 
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, MyRateLimiter rateLimiter) {
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        String rateLimiterKey = getRateKey(rateLimiter, point);
        log.error("rateLimiterKey == {}", rateLimiterKey);
        try {
            RateType rateType = RateType.OVERALL;
            if (rateLimiter.limitType() == LimitType.CLUSTER)
            {
                rateType = RateType.PER_CLIENT;
            }
 
            long number = -1;
            RRateLimiter rRateLimiter = redissonClient.getRateLimiter(rateLimiterKey);
            rRateLimiter.trySetRate(rateType, count, time, RateIntervalUnit.SECONDS);
            if (rRateLimiter.tryAcquire())
            {
                // 3.24
                 number = rRateLimiter.availablePermits();
            }
 
            if (number == -1) {
                String message = rateLimiter.errMsg();
                throw new RuntimeException(message);
            }
            log.info("限制令牌 => {}, 剩余令牌 => {}, 缓存key => '{}'", count, number, rateLimiterKey);
        } catch (Exception ex) {
            throw ex;
        }
    }
 
    /**
     * 解析El表达式获取lockKey
     * @param rateLimiter
     * @param joinPoint
     * @return
     */
    public String getRateKey(MyRateLimiter rateLimiter, JoinPoint joinPoint)
    {
        String key = rateLimiter.rateKey();
        // 获取方法(通过方法签名来获取)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        // 判断是否是spel格式
        if (StrUtil.containsAny(key, "#"))
        {
            // 获取参数值
            Object[] args = joinPoint.getArgs();
            // 获取方法上参数的名称
            String[] parameterNames = pnd.getParameterNames(method);
            if (ArrayUtil.isEmpty(parameterNames))
            {
                throw new RuntimeException("限流key解析异常!请联系管理员!");
            }
            for (int i = 0; i < parameterNames.length; i++)
            {
                context.setVariable(parameterNames[i], args[i]);
            }
            // 解析返回给key
            try
            {
                Expression expression;
                if (StrUtil.startWith(key, parserContext.getExpressionPrefix()) && StrUtil.endWith(key, parserContext.getExpressionSuffix()))
                {
                    expression = parser.parseExpression(key, parserContext);
                }
                else
                {
                    expression = parser.parseExpression(key);
                }
                key = expression.getValue(context, String.class) + ":";
            }
            catch (Exception e)
            {
                throw new RuntimeException("限流key解析异常!请联系管理员!");
            }
        }
        StringBuilder stringBuffer = new StringBuilder("xk-admin:rate_limit:");
 
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        stringBuffer.append(request.getRequestURI()).append(":");
        if (rateLimiter.limitType() == LimitType.IP)
        {
            // 获取请求ip
            stringBuffer.append(request.getRemoteAddr() + ":");
        }
        else if (rateLimiter.limitType() == LimitType.CLUSTER)
        {
            // 获取客户端实例id 3.24
            stringBuffer.append(redissonClient.getId()).append(":");
        }
        return stringBuffer.append(key).toString();
    }
}

四:限流注解中使用的枚举类

LimitType.java

java 复制代码
package com.modules.customannotations.enums;
 
public enum LimitType {
    /**
     * 默认限流策略,针对某一个接口进行限流
     */
    DEFAULT,
 
    CLUSTER,
    /**
     * 根据IP地址进行限流
     */
    IP;
}

五:测试一下

通过使用@RateLimiter 注解配置:count = 2, time = 10;即:每10秒钟产生2个令牌。

复制代码
   
java 复制代码
 @GetMapping("index/rateTest")
    @MyRateLimiter(rateKey = "'rateTest'+ #param1+#param2", count = 2, time = 10, limitType = LimitType.DEFAULT, errMsg = "访问超过限制,请稍后再试!")
    public void rateTest(String param1, String param2) {
        System.out.println("开始执行业务逻辑......");
        ThreadUtil.sleep(15000);
        System.out.println("结束执行业务逻辑......");
    }

浏览器访问:http://localhost:7001/java/index/rateTest

控制台输出:

使用redission实现自定义限流注解成功。

有好的建议,请在下方输入你的评论。

相关推荐
C++小厨神1 小时前
Java语言的循环实现
开发语言·后端·golang
Quantum&Coder2 小时前
Ruby语言的数据库编程
开发语言·后端·golang
ByteBlossom6662 小时前
Ruby语言的网络编程
开发语言·后端·golang
J不A秃V头A2 小时前
自定义SqlSessionFactory时mybatis-config.xml失效
java·开发语言
静水楼台x2 小时前
Java中json的一点理解
java·后端·json
晴空๓3 小时前
如何查看特定版本的Spring源码
java·spring boot·spring
Yeats_Liao4 小时前
Java List过滤 Stream API filter() 应用
java·开发语言·list
qingy_20464 小时前
【算法】图解二叉树的前中后序遍历
java·开发语言·算法
macrozheng4 小时前
Jenkins+Docker一键打包部署项目!步骤齐全,少走坑路!
java·spring boot·后端·docker·jenkins