基于Java编写一个限流工具类RateLimiter
作者:不归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限流工具类内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
