武功秘籍之限流

目录

“ 什么是限流?为什么要做限流?各个场景下限流功能是如何实现的?”

单机限流

限流:
在高并发环境下,往往一个服务会有多个业务方调用,不同的业务方请求频次不一样,业务重要程度也不一样,所以在设计分布式服务架构中,需要增加限流模块来保证核心主要服务的稳定性,本文主要介绍三种常用的限流方案,适合不同的应用场景。

场景描述:
对于大部分单实例或者几个实例的应用,服务可以采用单机限流的方式,在后端代码中设置固定的值,针对后端指定接口限制每秒或者每小时进行请求总量的限制。

解决方案:

为了更优雅的使用单机限流:采用【自定义注解+AOP+RateLimiter】组合的方式

自定义注解可便捷配置指定接口为限流接口

AOP可以方便实现接口限流统一业务逻辑处理

引入Guava提供的RateLimiter工具类,可以快速实现单机限流。该类主要基于“令牌桶”算法,控制指定时间内可以有多少令牌,获取到令牌的请求则可执行后续逻辑,否则就请求异常。

优点:

(1)开发便捷,使用服务机器内存,并发效率好。

缺点:

(1)由于初始化值在程序配置文件中定义,每次修改需要重启服务。

(2)不支持分布式环境下的接口总量控制。

实现过程:

(1)创建自定义限流注解


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiterAnnotation {
    /**
     *操作具体接口名称
     */
    String function() default "other";
}

(2)创建AOP切面类:定义切面类实现具体的限流逻辑


@Aspect
@Component
public class RateLimiterAop {
    /**
     * 限制请求速度公共类
     */
    private Map<String,RateLimiter> appIdRateLimiterMap = new  HashMap<String,RateLimiter>(20);
    RateLimiterAop(){
        //初始化限制请求权限
        appIdRateLimiterMap.put("test",RateLimiter.create(1));
        appIdRateLimiterMap.put("zxx",RateLimiter.create(10));
    }


    @Pointcut("@annotation(com.chow.kayadmin.core.ratelimiter.aop.single.RateLimiterAnnotation)")
    public void pointCutMethod() {
    }


    @Around(value = "pointCutMethod() && @annotation(annotation)")
    public Object collector(ProceedingJoinPoint joinPoint, RateLimiterAnnotation annotation) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        RateLimiterAnnotation rateLimiterAnnotation = methodSignature.getMethod().
            getAnnotation(RateLimiterAnnotation.class);
        if (rateLimiterAnnotation != null) {
            //获取注册上方传入的接口名称
            String src = "other";
            JSONObject requestJson = getParams(joinPoint);
            if (!ObjectUtils.isEmpty(requestJson)) {
                if (!StringUtils.isEmpty(requestJson.getString("src"))) {
                    src = requestJson.getString("src");
                }
            }
            RateLimiter rateLimiter =  appIdRateLimiterMap.get(src);
            if(!ObjectUtils.isEmpty(rateLimiter)&&!rateLimiter.tryAcquire()){
                //获取request
                HttpServletRequest request = ((ServletRequestAttributes)
                    RequestContextHolder.getRequestAttributes()).getRequest();
                String msg = "请求来源:"+src+",请求方IP:"+request.getRemoteAddr()
                    +",请求接口:"+request.getRequestURI()+",速率超过规定值(QPS/S):"+rateLimiter.getRate();
                throw new RateLimiterException(msg);
            }
            //如果传入的src不在定义的值里面,可以在这里处理,demo默认为通过
        }
        return joinPoint.proceed();
    }

    /**
     * 获取方法参数值并组装为JSONObject
     * @param joinPoint
     * @return
     */
    public static JSONObject getParams(JoinPoint joinPoint) {
        //获取参数值
        Object[] args = joinPoint.getArgs();
        if (ObjectUtils.isEmpty(args)) {
            return null;
        }
        JSONObject params = new JSONObject();
        //对象接收参数
        try {
            String data = JSON.toJSONString(joinPoint.getArgs()[0]);
            params = JSON.parseObject(data);
        }
        //普通参数传入
        catch (JSONException e){
            //获取参数名
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            for(int i=0;i<methodSignature.getParameterNames().length;i++){
                params.put(methodSignature.getParameterNames()[i],args[i]);
            }
        }
        return params;
    }
}

(3)在需要限流的接口上增加限流注解

   @RateLimiterAnnotation
    @PostMapping("/get/ids")
    public  Result genrateId(@RequestBody GenerateIdVO generateIdVO){
        if(ObjectUtils.isEmpty(generateIdVO.getNums())){
            throw new ParamsException("params nums is empty");
        }
        if(ObjectUtils.isEmpty(generateIdVO.getServiceId())){
            throw new ParamsException("params serviceId is empty");
        }
        if(generateIdVO.getNums() > MAX_NUMS || generateIdVO.getNums() <= 0){
            throw new ParamsException("nums cannot be greater than "+ MAX_NUMS+" or less than or equal 0");
        }
        if(generateIdVO.getServiceId() > GenerateIdUtils.MAX_SERVICE_ID
            ||generateIdVO.getServiceId() <0){
            throw new ParamsException("serviceId cannot be greater than "+GenerateIdUtils.MAX_SERVICE_ID+" or less than 0");
        }
        Set<Long> uidList = new HashSet<>();
        GenerateIdUtils generateId = new GenerateIdUtils(WORK_ID, generateIdVO.getServiceId());
        for (int i = 0; i < generateIdVO.getNums(); i++) {
            uidList.add(generateId.nextId());
        }
        return ResultUtils.success(uidList);
    }

动态单机限流

场景描述:
动态单机限流,作为单机限流的升级版,可以通过服务端配置文件(启动预加载,服务端主动推送两种同步配置方式),动态更新服务限流的配置信息,适用于限流阈值需要动态调整的场景。

解决方案:
【Redis+自定义注解+AOP+Ratelimiter】

与单机限流的区别就是每个接口的限流参数,通过服务端配置写到redis环境中,限流逻辑首先读取redis缓存中的接口限流参数,如果没有取到值,则取程序默认值。

优点:
1、开发便捷,使用服务机器内存,可动态配置限流参数,并发效率好。

缺点:
1、相对于单机限流,增加了一层redis服务会增加少量网络开销。
2、不支持分布式环境下的接口总量控制。

实现过程:
具体操作流程同单机模式,唯一的区别就是读取每个src来源的限流值,通过读取redis获取,如果没有获取到,则使用自定义的默认值进行处理。

1、创建限流参数-redis操作类

2、创建自定义限流注解

3、创建AOP切面类:定义切面类实现具体的限流逻辑

4、在需要限流的接口上增加限流注解

分布式限流

场景描述:
对于分布式服务环境下往往一个服务可能有几十个或者上百个实例,上述两个方式就无法对每个服务的接口进行精准的控制,所以采用分布式环境下的限流方案。

解决方案:
【自定义注解+AOP+Redis+Lua】

根据每个业务请求方的接口生成唯一身份key,并通过redis+lua设置对应key对应的值,如果该值超过redis中配置的限流阈值,则返回异常。

优点:
1、可以控制分布式环境下的接口请求总量。
2、可动态配置限流参数。

缺点:
1、增加redis的资源使用费用以及增加少量网络开销

实现过程:
该demo为非动态读取参数版本,如果需要动态读取参数,直接增加一个redis操作类,帮对应src来源的值种到redis,然后在aop类,读取redis中指定key的值进行控制即可。感兴趣的小伙伴可以自行研究一下。
1、创建限流参数-redis操作类

2、创建redis+lua脚本操作工具类

@Configuration
@EnableCaching
public class RateLimiterCommon {
    @Bean
    public DefaultRedisScript<Long> redisLuaScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
        redisScript.setResultType(Long.class);
        return redisScript;
    }
}

3、创建自定义限流注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiterRedisAnnotation {
    /**
     *操作具体接口名称
     */
    String function() default "other";
}

4、创建AOP切面类:使用redis+lua工具类进行限流控制


@Aspect
@Component
public class RateLimiterRedis {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private DefaultRedisScript<Long> redisLuaScript;

    /**
     * 限制请求速度公共类
     */
    private Map<String,Long> appIdRateLimiterMap = new HashMap<String,Long>(20);
    /**
     * 默认请求次数
     */
    private static final long DEFAULT_CONUTS =100L;
    /**
     * 默认请求时间 1秒
     */
    private static final long DEFAULT_LIMITER_TIME =1L;
    /**
     * 默认redis key前缀
     */
    private static final String DEFAULT_LIMITER_KEYS ="service:rateLimiter:";


    RateLimiterRedis(){
        //初始化限制请求权限
        appIdRateLimiterMap.put("test",1L);
        appIdRateLimiterMap.put("zxx",10L);
    }


    @Pointcut("@annotation(com.chow.kayadmin.core.ratelimiter.aop.redis.RateLimiterRedisAnnotation)")
    public void pointCutMethod() {
    }


    @Around(value = "pointCutMethod() && @annotation(annotation)")
    public Object collector(ProceedingJoinPoint joinPoint, RateLimiterRedisAnnotation annotation) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        RateLimiterRedisAnnotation rateLimiterRedisAnnotation = methodSignature.getMethod().
            getAnnotation(RateLimiterRedisAnnotation.class);
        if (rateLimiterRedisAnnotation != null) {
            //获取请求参数中的src
            String src = "other";
            JSONObject requestJson = getParams(joinPoint);
            if (!ObjectUtils.isEmpty(requestJson)) {
                if (!StringUtils.isEmpty(requestJson.getString("src"))) {
                    src = requestJson.getString("src");
                }
            }
            List<String> keys = Collections.singletonList(DEFAULT_LIMITER_KEYS+src);
            //如果使用动态参数,这里首先从redis取值,取不到则取系统的默认值
            Long counts = appIdRateLimiterMap.get(src);
            if(ObjectUtils.isEmpty(counts)||0==counts){
                counts =DEFAULT_CONUTS;
            }
            Long number = (Long) redisTemplate.execute(redisLuaScript, keys, counts, DEFAULT_LIMITER_TIME);

            if(number != null && 0==number.intValue()){
                //获取request
                HttpServletRequest request = ((ServletRequestAttributes)
                    RequestContextHolder.getRequestAttributes()).getRequest();
                String msg = "请求来源:"+src+",请求方IP:"+request.getRemoteAddr()
                    +",请求接口:"+request.getRequestURI()+",速率超过规定值(QPS/S):"+counts;
                throw new RateLimiterException(msg);
            }
            //如果传入的src不在定义的值里面,可以在这里处理,demo默认为通过
        }
        return joinPoint.proceed();
    }

    /**
     * 获取方法参数值并组装为JSONObject
     * @param joinPoint
     * @return
     */
    public static JSONObject getParams(JoinPoint joinPoint) {
        //获取参数值
        Object[] args = joinPoint.getArgs();
        if (ObjectUtils.isEmpty(args)) {
            return null;
        }
        JSONObject params = new JSONObject();
        //对象接收参数
        try {
            String data = JSON.toJSONString(joinPoint.getArgs()[0]);
            params = JSON.parseObject(data);
        }
        //普通参数传入
        catch (JSONException e){
            //获取参数名
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            for(int i=0;i<methodSignature.getParameterNames().length;i++){
                params.put(methodSignature.getParameterNames()[i],args[i]);
            }
        }
        return params;
    }

}

5、在需要限流的接口上增加限流注解

  @RateLimiterRedisAnnotation
    @PostMapping("/check/ids")
    public  Result checkIds(@RequestBody GenerateIdVO generateIdVO){
        if(ObjectUtils.isEmpty(generateIdVO.getIds())){
            throw new ParamsException("params nums is empty");
        }
        if(ObjectUtils.isEmpty(generateIdVO.getIds().size())){
            throw new ParamsException("nums cannot be greater than "+ MAX_NUMS);
        }
        List<GenerateId> generateIdList = new ArrayList<>(16);
        int idSize = generateIdVO.getIds().size();
        for (int i = 0; i < idSize; i++) {
            GenerateId generateId = new GenerateId();
            generateId.setId(generateIdVO.getIds().get(i));
            generateId.setServiceId(GenerateIdUtils.getServiceId(generateIdVO.getIds().get(i)));
            generateId.setWordId(GenerateIdUtils.getWorkerId(generateIdVO.getIds().get(i)));
            generateId.setSequence(GenerateIdUtils.getSequence(generateIdVO.getIds().get(i)));
            generateId.setTime(GenerateIdUtils.getTime(generateIdVO.getIds().get(i)));
            generateIdList.add(generateId);
        }
        return ResultUtils.success(generateIdList);
    }

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/zk18286047195/article/details/106052168