基于Java编写一个限流工具类RateLimiter

 更新时间:2024年01月26日 17:10:37   作者:不归SUN  
这篇文章主要为大家详细介绍了如何基于Java编写一个限流工具类RateLimiter,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

限流工具类RateLimiter

原理:令牌桶算法

有一个桶,桶的容量固定。系统以恒定的速度往桶里放令牌,令牌数不超过桶的容量。

用户发送请求进来,需要先从桶里获取一个令牌才能通过,获取后桶里令牌数减一。如果系统两秒一个往桶里放令牌,用户请求一秒一次,那么当令牌被取空后,操作就需要等待,会被限流。

可以应对突发流量,当桶里有足够多的令牌,可以一次处理多个请求

1.导入guava依赖包

<dependency>  
    <groupId>com.google.guava</groupId>  
    <artifactId>guava</artifactId>  
    <version>30.1-jre</version>  
</dependency>

RateLimiter的集个核心方法:create()tryAcquire()

  • acquire() 获取一个令牌, 会阻塞当前线程,直到获取到一个令牌。该方法返回值类型为 double,表示当前线程需要等待的时间(单位:秒),这个时间取决于令牌桶中令牌的剩余数量和发放速率。当执行 rateLimiter.acquire() 方法时,如果令牌桶中还有剩余的令牌,则该方法会立即返回,返回值为 0,表示当前线程无需等待即可获取到一个令牌。如果令牌桶中没有令牌,那么该方法就会阻塞当前线程,直到令牌桶中有令牌可用或者线程被中断。
  • acquire(int permits) 获取指定数量的令牌, 该方法也会阻塞, 返回值为获取到这 N 个令牌花费的时间
  • tryAcquire() 判断时候能获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits) 获取指定数量的令牌, 如果不能获取立即返回 false
  • tryAcquire(long timeout, TimeUnit unit) 判断能否在指定时间内获取到令牌, 如果不能获取立即返回 false
  • tryAcquire(int permits, long timeout, TimeUnit unit) 判断能否在指定时间内获取到指定数量的令牌, 如果不能获取立即返回 false
  • create(double permitsPerSecond) 创建每秒放入指定数量的令牌桶。SmoothBursty模式
  • create(5.0, 1, TimeUnit.SECONDS) 每秒5个令牌,预热时间1秒,SmoothWarmingUp模式

2.代码

public void limitTest(){  
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");  
  
//创建令牌桶,一秒一个,容量为1  
RateLimiter rateLimiter = RateLimiter.create(1);  
//获取放令牌的速率  
System.out.println("放令牌的速率:"+rateLimiter.getRate());  
for (int i=0;i<5;i++){  
//获取令牌,会阻塞,返回等待的时间  
double acquire = rateLimiter.acquire();  
System.out.println("第"+i+" 个令牌获取到的时间:"+LocalDateTime.now().format(dtf)+",等待时间:"+acquire);  
}  
  
//是否会立即获取到令牌  
boolean tryAcquire = rateLimiter.tryAcquire();  
}

3.AOP+RateLimiter+注解,实现限流

1.创建注解

public @interface Limit {  
// 资源主键  
String key() default "";  
//最多访问次数,代表请求总数量  
double permitsPerSeconds();  
// 时间:即timeout时间内,只允许有permitsPerSeconds个请求总数量访问,超过的将被限制不能访问  
long timeout();  
//时间类型,默认秒
TimeUnit timeUnit() default TimeUnit.SECONDS;  
  
//提示信息  
String msg() default "系统繁忙,请稍后重试";  
}

2.AOP切面

@Aspect  
@Component  
public class LimitAop {  
  
private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();  
  
private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");  

@Around("@annotation(com.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;  
        //验证缓存是否有命中key  
        if (!limitMap.containsKey(key)) {  
            //创建令牌桶  
            rateLimiter = RateLimiter.create(limit.permitsPerSeconds());  
            limitMap.put(key, rateLimiter);  
            log.info("新建了令牌桶={},容量={}", key, limit.permitsPerSeconds());  
        }  
        rateLimiter = limitMap.get(key);  
        //拿一个令牌,拿不到会一直阻塞 
        double acquire = rateLimiter.acquire(1);  
        log.info("{},获取令牌时间{}", key,LocalDateTime.now().format(dtf));  
        
        /*
        //是否能立即拿到令牌,不能则桶里没有,还没到一秒钟,进行限流
        boolean acquire = rateLimiter.tryAcquire();  
        if (!acquire) {  
        log.info("令牌桶={},获取令牌失败", key);  
        throw new RuntimeException(limit.msg());  
        }*/  
        
        }  
    return joinPoint.proceed();  
    }  
}

3.注解使用

@GetMapping("/limitTest")  
@Limit(key = "limitTest",permitsPerSeconds = 1,timeout = 1,msg = "触发接口限流,请重试")  
public void limitTest(){  
    //其它逻辑代码  
    //这里打印调用该接口执行的时间,以便观察限流  
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");  
    System.out.println(LocalDateTime.now().format(dtf));  
}

日志情况:

无论请求速度多快,一秒后才能处理请求。因为rateLimiter.acquire(1)拿不到,一直等待,会阻塞。 其它获取令牌的方法感兴趣可以按照上文方法详情自行测试。

到此这篇关于基于Java编写一个限流工具类RateLimiter的文章就介绍到这了,更多相关Java限流工具类内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Mybatis之RowBounds分页原理详解

    Mybatis之RowBounds分页原理详解

    这篇文章主要介绍了Mybatis之RowBounds分页原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • 解决Spring Security集成knife4j访问接口文档出现403的问题

    解决Spring Security集成knife4j访问接口文档出现403的问题

    这篇文章主要给大家介绍了如何解决Spring Security集成knife4j访问接口文档出现403的问题,文中有详细的解决方案,有需要的朋友可以参考阅读下
    2023-07-07
  • Java获取链接上的参数三种方法举例

    Java获取链接上的参数三种方法举例

    在Java中,我们经常需要从URL链接中获取参数,下面这篇文章主要给大家介绍了关于Java如何获取链接上参数的三种方法,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-06-06
  • Java判空的一些常见方法

    Java判空的一些常见方法

    这篇文章主要给大家分享介绍了Java判空的一些常见方法,在程序中必须进行严格的判空处理,避免对空对象的异常操作,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • MybatisPlus 主键策略之type=IdType.ASSIGN_ID等详解

    MybatisPlus 主键策略之type=IdType.ASSIGN_ID等详解

    雪花算法(雪花)是微博开源的分布式ID生成算法其核心思想就是:使用一个64位的长型的数字作为全局唯一ID,这篇文章主要介绍了MybatisPlus 主键策略(type=IdType.ASSIGN_ID等详解),需要的朋友可以参考下
    2024-04-04
  • mybatis xml注释sql的注意事项及说明

    mybatis xml注释sql的注意事项及说明

    这篇文章主要介绍了mybatis xml注释sql的注意事项及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • 深入理解 Java、Kotlin、Go 的线程和协程

    深入理解 Java、Kotlin、Go 的线程和协程

    这篇文章主要介绍了深入理解 Java、Kotlin、Go 的线程和协程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • Java8 使用工厂方法supplyAsync创建CompletableFuture实例

    Java8 使用工厂方法supplyAsync创建CompletableFuture实例

    这篇文章主要介绍了Java8 使用工厂方法supplyAsync创建CompletableFuture实例,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • SpringBoot异常: nested exception is java.lang.NoClassDefFoundError: javax/servlet/ServletContext解决方案

    SpringBoot异常: nested exception is java.lang.NoClassDefFoundE

    这篇文章主要介绍了SpringBoot异常: nested exception is java.lang.NoClassDefFoundError: javax/servlet/ServletContext解决方案,说明了错误原因和解决方案,需要的朋友可以参考下
    2021-06-06
  • Jetty启动项目中引用json-lib相关类库报错ClassNotFound的解决方案

    Jetty启动项目中引用json-lib相关类库报错ClassNotFound的解决方案

    今天小编就为大家分享一篇关于Jetty启动项目中引用json-lib相关类库报错ClassNotFound的解决方案,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-12-12

最新评论