服务限流 -- 自定义注解基于RateLimiter实现接口限流

1. 令牌桶限流算法

令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌,当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个友好的错误信息给用户。

2. RateLimiter简单实现
maven依赖

        <!-- guava  -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>

本人使用的是SpringBoot 2.0.4.RELEASE,Jdk1.8环境下编写,部分代码贴出:

   /**
     * 以1r/s往桶中放入令牌
     */
    private RateLimiter limiter = RateLimiter.create(1.0);

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @GetMapping("/indexLimiter")
    public String indexLimiter() {
        // 如果用户在500毫秒内没有获取到令牌,就直接放弃获取进行服务降级处理
        boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
        if (!tryAcquire) {
            log.info("Error ---时间:{},获取令牌失败.", sdf.format(new Date()));
            return "系统繁忙,请稍后再试.";
        }
        log.info("Success ---时间:{},获取令牌成功.", sdf.format(new Date()));
        return "success";
    }

调用结果如下:

使用RateLimiter注意的地方:

允许先消费,后付款,意思就是它可以来一个请求的时候一次性取走几个或者是剩下所有的令牌甚至多取,但是后面的请求就得为上一次请求买单,它需要等待桶中的令牌补齐之后才能继续获取令牌。

3.自定义注解实现基于接口限流


仔细看会发现上面的简单实现会造成我每个接口都要写一次限流方法代码很冗余,所以采用aop来使用自定义注解来实现。


maven依赖

        <!-- aop  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- google guava 依赖  -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>
        <!-- lombok 简化java代码-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

首先定义一个自定义注解:

package com.limiting.annotation;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface AnRateLimiter {

    //以固定数值往令牌桶添加令牌
    double permitsPerSecond () ;

    //获取令牌最大等待时间
    long timeout();

    // 单位(例:分钟/秒/毫秒) 默认:毫秒
    TimeUnit timeunit() default TimeUnit.MILLISECONDS;

    // 无法获取令牌返回提示信息 默认值可以自行修改
    String msg() default "系统繁忙,请稍后再试.";
}

然后使用aop的环绕通知来拦截注解,使用了一个ConcurrentMap来保存每个请求对应的令牌桶,key是没有url请求,防止出现每个请求都会新建一个令牌桶这么会达不到限流效果.

package com.limiting.aspect;

import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import com.limiting.annotation.AnRateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;

/**
 *  * 描述:
 *
 * @author 只写BUG的攻城狮
 *  * @date 2018-09-12 12:07
 */
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
    /**
     * 使用url做为key,存放令牌桶 防止每次重新创建令牌桶
     */
    private Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
    
    @Pointcut("@annotation(com.limiting.annotation.AnRateLimiter)")
    public void anRateLimiter() {
    }

    @Around("anRateLimiter()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取request,response
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        // 或者url(存在map集合的key)
        String url = request.getRequestURI();
        // 获取自定义注解
        AnRateLimiter rateLimiter = getAnRateLimiter(joinPoint);
        if (rateLimiter != null) {
            RateLimiter limiter = null;
            // 判断map集合中是否有创建有创建好的令牌桶
            if (!limitMap.containsKey(url)) {
                // 创建令牌桶
                limiter = RateLimiter.create(rateLimiter.permitsPerSecond());
                limitMap.put(url, limiter);
                log.info("<<=================  请求{},创建令牌桶,容量{} 成功!!!", url, rateLimiter.permitsPerSecond());
            }
            limiter = limitMap.get(url);
            // 获取令牌
            boolean acquire = limiter.tryAcquire(rateLimiter.timeout(), rateLimiter.timeunit());

            if (!acquire) {
                responseResult(response, 500, rateLimiter.msg());
                return null;
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 获取注解对象
     * @param joinPoint 对象
     * @return ten LogAnnotation
     */
    private AnRateLimiter getAnRateLimiter(final JoinPoint joinPoint) {
        Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
        String name = joinPoint.getSignature().getName();
        if (!StringUtils.isEmpty(name)) {
            for (Method method : methods) {
                AnRateLimiter annotation = method.getAnnotation(AnRateLimiter.class);
                if (!Objects.isNull(annotation) && name.equals(method.getName())) {
                    return annotation;
                }
            }
        }
        return null;
    }

    /**
     * 自定义响应结果
     *
     * @param response 响应
     * @param code     响应码
     * @param message  响应信息
     */
    private void responseResult(HttpServletResponse response, Integer code, String message) {
        response.resetBuffer();
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.println("{\"code\":" + code + " ,\"message\" :\"" + message + "\"}");
            response.flushBuffer();
        } catch (IOException e) {
            log.error(" 输入响应出错 e = {}", e.getMessage(), e);
        } finally {
            if (writer != null) {
                writer.flush();
                writer.close();
            }
        }
    }
}

最后来试试自己定义的注解是否生效,能否达到限流效果.

    @GetMapping("/index")
    @AnRateLimiter(permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "亲,现在流量过大,请稍后再试.")
    public String index() {
        return System.currentTimeMillis() + "";
    }

访问请求(按F5狂刷新浏览器)效果如下图:

总结
至此已基本上使用注解实现了接口限流,后期可以根据自己需求自行修改,这个只适于单个应用进行接口限流,如果是分布式项目或者微服务项目可以采用redis来实现,后期有时间来一个基于redis自定义注解来实现接口限流。
本人也是刚入Java开发行业没多久的小菜鸟,在文章中可能存在一些说的不对,代码不严谨的地方欢迎各位大神指出,本人表示由衷的感谢和耐心的学习,希望能在开发中给大家一些帮助。

转载
https://segmentfault.com/a/1190000016393473

猜你喜欢

转载自www.cnblogs.com/chongaizhen/p/11125485.html