前言
没事儿闲得慌,看看同事的代码吧。我们的项目通过lua脚本实现了一套限流的插件。我这次简单的看了一下实现的逻辑,wtfk这是啥呀! 好几个bug,而且这个代码已经在好几个项目中用了,都是cv过来cv过去。。。。。
下面就分析一下我的同事是怎么实现的,存在哪些问题。最后对限流做个小小的总结
问题代码
问题代码实现限流的方式是通过利用AOP对接标记注解的方法进行拦截,通过lua
脚本操作redis
来保存接口的执行次数。
注解类
仔细看注释,看注释就是令牌桶限流
public @interface RateLimiter {
//往令牌桶放入令牌的速率
double value() default Double.MAX_VALUE;
//获取令牌的超时时间
double limit() default Double.MAX_VALUE;
}
AOP核心实现
key
值取得是当前时间秒数
RateLimiter rateLimiter = signature.getMethod().getDeclaredAnnotation(RateLimiter.class);
if(rateLimiter == null){
//正常执行方法,执行正常业务逻辑
return proceedingJoinPoint.proceed();
}
//获取注解上的参数,获取配置的速率
double value = rateLimiter.value();
double time = rateLimiter.limit();
//list设置lua的keys[1]
//取当前时间戳到单位秒
String key = "ip:"+ System.currentTimeMillis() / 1000;
List<String> keyList = CollUtil.newArrayList(key);
//调用脚本并执行
List result = stringRedisTemplate.execute(redisScript, keyList, String.valueOf(value),String.valueOf(time));
//lua 脚本返回 "0" 表示超出流量大小,返回1表示没有超出流量大小
if(StringUtils.equals(result.get(0).toString(),"0")){
//服务降级
fullback();
return null;
}
lua脚本
看看过期时间
local key = KEYS[1] --限流KEY
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call('get', key) or "0") if current + 1 > limit then
return 0 else redis.call("INCRBY", key,"1") redis.call("expire", key,"2") return current + 1 end
看到这儿你发现了几个bug呢?????
问题分析
- 从注解中的第一个参数
value:往令牌桶放入令牌的速率
,限流实现的算法感觉是令牌桶算法
,注解的参数也没看看到有设置桶容量的参数。(看来不是一个正确的漏桶算法) -
AOP
代码中用当前时间的秒数作为存放请求次数的redis
key
,时间做为key,造成了不同接口的限流数据在同一秒内共享了啊。同一秒中,其他标记限流的接口进来,又是接着上一次限流次数进行计算。 - lua脚本,传了两个值,只用了一个限制次数,另一个时间参数没有使用。看了
lua
脚本呢才知道它实现的限流方式是固定时间窗口
限流。
看完三个代码块,才明白它实现的是固定时间窗口
的限流算法。只是注解的注释不对,被误导了呀!!!!
调整之后
- 修改注释,数据类型调整成int吧。直接传小数到lua脚中有数据类型问题
public @interface RateLimiter {
//limit 时间内 最大请求次数
int value() default 1000;
//限制时间 秒
int limit() default 1;
}
- AOP中可以的可以的生成规则改为方法名称,这样各个接口的限流数据就分开了。同时获取注解参数的代码改为
int
接收
String key = REDIS_PRE+"limitMethod:"+ proceedingJoinPoint.getSignature().getName();
//time 秒内累计请求次数
int value = redisRateLimiter.value();
//key过期时间
int time = redisRateLimiter.limit();
3.lua脚本获取时间参数
redis.call("expire", key,ARGV[1])
总结
在实际开发中遇到要限流的需求,我们应该首先考虑了解限流算法实现方式有哪些,业务场景适合哪种算法,最后是考虑用什么组件方便。
常见的限流算法有以下几种:
- 令牌桶算法 (Token Bucket)
- 原理:令牌桶算法使用一个桶来存放令牌,令牌以固定的速率生成。当请求到来时,必须从桶中获取一个令牌才能被处理。如果桶空了,请求会被拒绝或等待。
- 特点:可以处理突发流量,适合需要平滑流量的场景。
- 漏桶算法 (Leaky Bucket)
- 原理:漏桶算法将请求放入一个固定容量的桶中,桶以恒定的速率“漏水”。即使请求以高峰值到达,处理也会平滑到漏水的速率。
- 特点:控制输出速率,适合对流量有严格限制的场景。
- 计数器算法 (Counter)/固定窗口
- 原理:在一定时间窗口内,记录请求的数量。一旦请求数量超过设定的阈值,后续请求会被拒绝。
- 特点:简单易用,适合请求速率相对稳定的场景。
- 滑动窗口算法 (Sliding Window)
- 原理:在一定时间窗口内维护一个请求计数器,允许在窗口内的请求数量超过阈值,但在时间窗口的不同部分可以有不同的请求数。
- 特点:相比计数器算法,能更灵活地处理请求,适合对请求速率有动态要求的场景。
- 基于时间的限流
- 原理:根据时间戳来限制请求。例如,每秒只允许处理一定数量的请求。
- 特点:适合需要严格限制请求频率的场景。
在 Java 中,常用的限流组件主要有以下几种,它们各自实现限流的原理和机制略有不同:
常见的限流中间件
- Guava RateLimiter
-
原理:基于令牌桶算法实现。
RateLimiter
允许你设定一个固定的速率(如每秒生成多少个令牌),并在请求到达时判断是否可以获取到令牌。如果获取到令牌,允许请求通过;否则请求会被阻塞或延迟。 - 特点:简单易用,适合在应用程序中直接集成。
- Resilience4j RateLimiter
- 原理:同样基于令牌桶算法,支持更复杂的配置和状态管理。它不仅可以限制请求速率,还可以监控和管理请求失败的情况。
- 特点:具有分布式环境下的灵活性和可靠性,适合微服务架构。
- Spring Cloud Gateway RateLimiter
- 原理:使用 Redis 或其他存储系统来实现限流,支持基于请求的限流策略。它可以根据请求的 IP 地址、请求路径等信息进行动态限流。
- 特点:适合微服务场景,能够根据不同条件灵活配置限流规则。
- Hystrix (虽然已不再维护)
- 原理:Hystrix 的限流是通过熔断器模式实现的,尽管它主要用于服务的容错和隔离,但也可以限制请求的并发数量。
- 特点:适合在微服务架构中使用,能够提高系统的稳定性。
- Bucket4j
- 原理:基于漏桶算法实现,支持在内存和分布式环境中使用。可以配置桶的容量、漏水速率等参数。
- 特点:灵活性高,适合复杂的限流需求。
- Redis 限流
- 原理:利用 Redis 的原子自增和过期特性实现限流。通过设置一个计数器,记录在特定时间窗口内的请求数量,超过阈值则拒绝请求。
- 特点:适合分布式系统,可以跨多个实例共享限流状态。