随笔 - 632  文章 - 17  评论 - 54  阅读 - 93万

使用Guava实现单体应用限流

一、概述

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

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

  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.使用原生实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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 "已拿到令牌,请求成功";
    }
}
复制代码

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

 

posted on   飘杨......  阅读(280)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
历史上的今天:
2021-09-27 使用C++实现简单的服务器示例
2016-09-27 Android向系统相册中插入图片,相册中会出现两张 一样的图片(只是图片大小不一致)
2013-09-27 Android 混淆打包
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示