百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

guava限流器RateLimiter使用简介(Springboot实现)

csdh11 2025-03-12 13:41 16 浏览

在大型分布式系统中,限流是一种重要的防护机制,可以帮助我们控制流量并减轻系统的负担。Google的Guava库提供了一种方便的限流器实现,可以帮助我们轻松地实现限流功能。本文将介绍Guava中限流器的基本概念和使用方法。

一、什么是限流器?

限流器是一种控制系统流量的机制,可以帮助我们在高负载情况下保护系统免受过载和崩溃的风险。通过限制进入系统的请求速率,我们可以避免服务器过载和资源耗尽的问题。

二、Guava中限流器的实现原理

Guava中的限流器实现基于令牌桶算法,它是一种流量控制算法,可以帮助我们平滑地限制请求速率。限流器将请求速率限制为指定的速率,以确保系统能够在高负载情况下保持稳定。
Guava中的限流器使用一个桶来保存令牌,桶中的令牌代表可用的请求数量。每当一个请求到达时,限流器会尝试从桶中取出一个令牌,如果桶中没有可用的令牌,则该请求将被阻塞,直到有足够的令牌可用为止。当令牌被用尽时,限流器将根据配置的速率自动添加新的令牌到桶中,以确保系统能够继续处理请求。

三、如何使用Guava中的限流器?

在Guava中使用限流器非常简单,只需要按照以下步骤即可:

1.创建一个限流器对象

Guava中的限流器对象是通过RateLimiter类实现的。您可以使用RateLimiter.create(double permitsPerSecond)方法创建一个限流器对象,其中参数permitsPerSecond是每秒允许的请求数。

例如,以下代码创建一个每秒允许10个请求的限流器对象:

`RateLimiter limiter =RateLimiter.create(10);

2.获取许可证

当您的应用程序需要处理一个请求时,可以使用RateLimiter.acquire()方法获取许可证。这个方法将会阻塞,直到有足够的令牌可用。如果限流器没有可用的令牌,则该方法将阻塞,直到有足够的令牌可用。

例如,以下代码获取一个许可证并处理请求:

limiter.acquire();
handleRequest();

3.尝试获取许可证

如果您希望尝试获取许可证而不是阻塞,则可以使用RateLimiter.tryAcquire()方法。这个方法将尝试获取许可证,如果限流器没有可用的令牌,则该方法将返回false,否则将返回true。

例如,以下代码尝试获取一个许可证:

if (limiter.tryAcquire()) {
    handleRequest();
} else {
    handleRateLimitedRequest();
}

4.调整速率

您还可以使用RateLimiter.setRate(double permitsPerSecond)方法动态地调整限流器的速率。这个方法可以帮助您根据系统负载和流量需求动态调整请求速率。

例如,以下代码动态调整限流器的速率:

limiter.setRate(20);

5.处理多个请求

如果您需要处理多个请求,则可以使用RateLimiter.acquire(int permits)方法获取多个许可证。这个方法将会阻塞,直到有足够的令牌可用。

例如,以下代码获取5个许可证并处理5个请求:

limiter.acquire(5);
handleRequest1();
handleRequest2();
handleRequest3();
handleRequest4();
handleRequest5();

四、Guava限流器的注意事项

在使用Guava限流器时,需要注意以下几点:

  1. 限流器的速率应该根据系统负载和流量需求进行动态调整。
  2. 如果您需要处理多个请求,请确保您的限流器对象能够处理足够的请求数量。
  3. 在高负载情况下,限流器可能会导致请求超时或错误,请确保您的应用程序能够处理这些情况。
  4. 限流器的速率应该设置为适当的值,以确保系统能够处理所有的请求。

五、总结

Guava中的限流器提供了一种简单而有效的方法来控制系统流量,以保护系统免受过载和崩溃的风险。使用Guava限流器,您可以轻松地实现限流功能,并根据系统负载和流量需求动态调整请求速率。在实现分布式系统时,使用限流器是一种重要的防护机制,可以帮助我们确保系统的稳定性和可靠性。

六、SpringBoot使用限流器示例

1、pom文件添加依赖


com.google.guava
guava
30.1-jre

2、给接口加上限流逻辑

@Slf4j
@RestController
@RequestMapping("/limit")
public class LimitController {
    /**
     * 限流策略 :1秒钟2个请求
     */
    private final RateLimiter limiter = RateLimiter.create(2.0);
 
    private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
    @GetMapping("/test1")
    public String testLimiter() {
        // 500毫秒内,没拿到令牌,就直接进入服务降级
        boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
 
        if (!tryAcquire) {
            log.warn("进入服务降级,时间{}", LocalDateTime.now().format(dtf));
            return "当前排队人数较多,请稍后再试!";
        }
 
        log.info("获取令牌成功,时间{}", LocalDateTime.now().format(dtf));
        return "请求成功";
    }
}

以上用到了RateLimiter的2个核心方法:create()、tryAcquire(),以下为详细说明

  • acquire() 获取一个令牌, 改方法会阻塞直到获取到这一个令牌, 返回值为获取到这个令牌花费的时间
  • acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间
  • tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 false
  • tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits, long timeout, TimeUnit unit) 同上

3、体验效果

通过访问测试地址:http://127.0.0.1:8080/limit/test1,反复刷新并观察后端日志

WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:37
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:37
 
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38
WARN  LimitController:35 - 进入服务降级,时间2021-09-25 21:39:38
INFO  LimitController:39 - 获取令牌成功,时间2021-09-25 21:39:38

从以上日志可以看出,1秒钟内只有2次成功,其他都失败降级了,说明已经成功给接口加上了限流功能。

七、SpringBoot基于AOP实现接口限流

1、加入AOP依赖


  org.springframework.boot
  spring-boot-starter-aop

2、自定义限流注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {
    /**
     * 资源的key,唯一
     * 作用:不同的接口,不同的流量控制
     */
    String key() default "";
 
    /**
     * 最多的访问限制次数
     */
    double permitsPerSecond () ;
 
    /**
     * 获取令牌最大等待时间
     */
    long timeout();
 
    /**
     * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒
     */
    TimeUnit timeunit() default TimeUnit.MILLISECONDS;
 
    /**
     * 得不到令牌的提示语
     */
    String msg() default "系统繁忙,请稍后再试.";
}

3、使用AOP切面拦截限流注解

@Slf4j
@Aspect
@Component
public class LimitAop {
    /**
     * 不同的接口,不同的流量控制
     * map的key为 Limiter.key
     */
    private final Map limitMap = Maps.newConcurrentMap();
 
    @Around("@annotation(com.jianzh5.blog.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) {
            //key作用:不同的接口,不同的流量控制
            String key=limit.key();
            RateLimiter rateLimiter = null;
            //验证缓存是否有命中key
            if (!limitMap.containsKey(key)) {
                // 创建令牌桶
                rateLimiter = RateLimiter.create(limit.permitsPerSecond());
                limitMap.put(key, rateLimiter);
                log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
            }
            rateLimiter = limitMap.get(key);
            // 拿令牌
            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
            // 拿不到命令,直接返回异常提示
            if (!acquire) {
                log.debug("令牌桶={},获取令牌失败",key);
                this.responseFail(limit.msg());
                return null;
            }
        }
        return joinPoint.proceed();
    }
 
    /**
     * 直接向前端抛出异常
     * @param msg 提示信息
     */
    private void responseFail(String msg)  {
        HttpServletResponse response=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        ResultData<Object> resultData = ResultData.fail(ReturnCode.LIMIT_ERROR.getCode(), msg);
        WebUtils.writeJson(response,resultData);
    }
}

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

@Slf4j
@RestController
@RequestMapping("/limit")
public class LimitController {
    
    @GetMapping("/test2")
    @Limit(key = "limit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "当前排队人数较多,请稍后再试!")
    public String limit2() {
        log.info("令牌桶limit2获取令牌成功");
        return "ok";
    }
 
 
    @GetMapping("/test3")
    @Limit(key = "limit3", permitsPerSecond = 2, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "系统繁忙,请稍后再试!")
    public String limit3() {
        log.info("令牌桶limit3获取令牌成功");
        return "ok";
    }
}

5、体验效果

通过访问测试地址:http://127.0.0.1:8080/limit/test2,反复刷新并观察输出结果:

正常响应时:

{"status":100,"message":"操作成功","data":"ok","timestamp":1632579377104}

触发限流时:

{"status":2001,"message":"系统繁忙,请稍后再试!","data":null,"timestamp":1632579332177}

通过观察得之,基于自定义注解同样实现了接口限流的效果。

6、小结

一般在系统上线时我们通过对系统压测可以评估出系统的性能阈值,然后给接口加上合理的限流参数,防止出现大流量请求时直接压垮系统。今天我们介绍了几种常见的限流算法(重点关注令牌桶算法),基于Guava工具类实现了接口限流并利用AOP完成了对限流代码的优化。

相关推荐

教学楼里那种嵌着小石子的水磨石地面,是怎么整出来的? | 有趣的制造

今天的选题是之前小可爱「花凉」在后台发消息问的~看过以后念念不忘,满脑子都是小时候在教学楼冰冷地面上摔的跤,记不起来是不是在这种地面上磕掉的门牙...昨天发了预告后,有小可爱纷纷表示「就是这种地板,像...

教学楼里那种嵌着小石子的水磨石地面,是怎么整出来的?

话说有多少小可爱不想学习时,没事数着水磨石地面的小石子玩,然后互相评比哪颗石子最好看。到头来书又没有背完,课也没好好上,就怪地板有迷幻效果,扰乱了好好学习的坚定意志。(小编觉得即使换成瓷砖,你们也可能...

性能调优实战:Spring Boot 多线程处理SQL IN语句大量值的优化方案

环境:SpringBoot3.4.0...

RMAN备份监控及优化总结(rman全备份)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

记Oracle中快速获取表及其各个字段注释的方法

简述java开发中,用过JPA的道友应该知道,我们可以通过写java代码自动生成对应的数据表;但这有个问题是,列名的注释并没有帮我们一起添加到数据库去,尤其在一些开发测试生产三个环境隔离的,就很不友好...

Oracle 数据库日常巡检之检查数据库cpu、I/O、内存性能

记录数据库的cpu使用、IO、内存等使用情况,使用vmstat,iostat,sar,top等命令进行信息收集并检查这些信息,判断资源使用情况。1.CPU使用情况:...

Oracle案例:ORA-00600: internal error code, arguments: 「4187」

本案例客户来自某省电信,alert日志大量的ORA-00600[4187]报错,已经影响到业务正常运行。...

MySQL索引失效的10大陷阱:从隐式类型转换到索引选择性全面优化

索引是MySQL性能优化的核心武器,但错误的使用场景可能让索引完全失效,导致查询性能断崖式下降。本文通过实际案例,深入剖析索引失效的典型场景及其底层原理,并提供可落地的解决方案。一、索引失效的核心原...

oracle查询语句执行计划分析(oracle如何查看sql执行计划)

1命令行开启配置#显示查询结果setautotraceon#不显示查询结果setautotracetraceonly2执行查询语句...

面试官:说说Oracle数据库result cache的原理是什么?

概述前面已经用实验给大家介绍了ResultCache相关内容,今天主要讨论一下Oracle11gResultCache的深层原理。从参数看,Oracle提供了ClientResultCac...

Oracle817 export 时ORA-06553和ORA-00904处理

现象:数据库版本8.1.7...

Oracle案例:一次gc buffer busy acquire诊断

本案例来自某客户两节点rac的一次生产故障,现象是大面积的gcbufferbusyacquire导致业务瘫痪。...

说文解字:“雪”字本身在造字时就很浪漫!

这是雪山的“雪”字。可是你知道吗?“雪”这个字其实和“山”是没有任何关系的。这个字下半部分“彐”并不是一座翻倒的山,而是一只手的意思。(凡是带“彐”的汉字,其实都和手有关。)“雪”字的商代甲骨文形状,...

应用最广的两类数据库的区别、优势对比、查询优化方法及案例实践

 1、通用数据库分类  1.1关系型数据库  关系型数据库是多个二维数据表的集合,数据以二维数据表的形式进行存储,数据表之间可以通过应用程序或者数据的主、外键建立特定的关联关系,让数据之间存在特定的...

【SQL】SQL 语法差异大全(PgSQL/MySQL/Oracle/TiDB/OceanBase)

以下是针对不同数据库系统的SQL语法差异总结,按功能分类展示:一、基础查询1.分页查询...