使用Guava实现单体应用限流

发布时间 2023-09-27 11:49:25作者: 飘杨......

一、概述

  服务器流量控制一直都是一个非常重要的问题。因为服务器是有性能瓶颈的,所以后台的接口也有其性能瓶颈,当辛辛苦苦的把多级缓存做好后,觉得可以承受高并发了的时候,服务突然就蹦了,可能是缓存爆掉了,也可能是数据库宕机了。造成这些问题的大多数原因就是流量太高了的问题。当然我们也可以进行服务的分布式部署。但今天不讨论这个,就讨论单体应用。

  单体应用的限流算法还是比较多的。

  1.AtomicInteger自己手动实现

  2.漏铜算法

  3.令牌算法

  今天就介绍下令牌算法,而且使用Guava框架自带的。

  原理如下:

系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,
则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。
令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。

 

二、示例(两种方法实现,其一是原生,其二是使用注解帮我们简化)

  1.引入guava和apo

   <!--本地缓存for guava cache-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>24.0-jre</version>

 <!--        面向切面编程-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

  2.使用原生实现

@Slf4j
@RestController
@RequestMapping("/api/v1/pub/limit/")
public class GuavaCurrentLimitingController {
    /**
     * 限流策略:1秒钟 2个请求(这里表示这个接口1秒钟服务器最多接收两个并发,其他请求直接返回人说过多的提示。)
     */
    private final RateLimiter limiter = RateLimiter.create(2.0);
    private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @GetMapping("/test")
    public void test() {
        //500毫秒内没拿到令牌就进行服务降级
        boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MICROSECONDS);
        //如果没拿到令牌就进行服务降级
        if (!tryAcquire) {
            log.info("当前排队的人数过多,请稍后再试{}", LocalDateTime.now().format(dtf));
        }

        log.info("请求成功{}", LocalDateTime.now().format(dtf));
    }
}

  上述代码可以解决单体应用的简单限流问题。但是代码写的不够优雅。

  下面借助AOP+注解的形式对代码进行优化

  1.定义个Limit注解

/**
 * Guava自定义限流注解
 */
@Retention(RetentionPolicy.RUNTIME)//运行时注解
@Target({ElementType.METHOD})//注解用在方法上
@Documented
public @interface Limit {
    /**
     * 唯一id
     * 作用:不同的接口执行不同的限流策略
     */
    String id() default "";

    /**
     * 最多访问次数限制
     */
    double permitsPerSecond();

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

    /**
     * 获取令牌最大等待时间单位(默认毫秒)
     */
    TimeUnit timeunit() default TimeUnit.MILLISECONDS;

    /**
     * 无法获取令牌的提示语
     */
    String errMsg() default "当前请求人数过多,请稍后再试";


}

  2.定义一个AOP类,来解析这个注解

/**
 * 使用自定义guava限流注解
 *
 * @author Tony
 * @version 2023
 * @date 2023/9/27 10:56
 */
@Slf4j
@Aspect
@Component
public class GuavaLimit {
    /**
     * 存储不同接口不同限流策略的map
     */
    private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();

    @Around("@annotation(com.tony.cursor.limit.Limit)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //拿limit的注解
        Limit limit = method.getAnnotation(Limit.class);
        if (limit != null) {
            //id作用:不同的接口,不同的流量控制
            String id = limit.id();
            RateLimiter rateLimiter = null;
            //验证缓存是否有命中key
            if (!limitMap.containsKey(id)) {
                // 创建令牌桶
                rateLimiter = RateLimiter.create(limit.permitsPerSecond());
                limitMap.put(id, rateLimiter);
                log.info("新建了令牌桶={},容量={}", id, limit.permitsPerSecond());
            }
            rateLimiter = limitMap.get(id);
            // 拿令牌
            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
            // 拿不到命令,直接返回异常提示
            if (!acquire) {
                log.info("令牌桶={},获取令牌失败", id);
                throw new CustomException(limit.errMsg());//如果没有拿到令牌就直接抛异常
            }
        }
        return joinPoint.proceed();
    }

}

  3.使用注解进行限流

@Slf4j
@RestController
@RequestMapping("/api/v1/pub/limit/")
public class GuavaCurrentLimitingController {

    @GetMapping("/test2")
    @Limit(id = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,
            errMsg = "当前排队人数过多,请稍后再试")
    public String test2() {
        log.info("拿到令牌请求成功");
        return "已拿到令牌,请求成功";
    }
}

  好了,优化完毕,测试结果如下所示: