- 作者:老汪软件技巧
- 发表时间:2024-11-13 10:03
- 浏览量:
前言
最近我要对外提供一套接口,所以接口的调用频率肯定是要限制的。我们的项目也通过lua脚本实现了一套限流的插件。我这次简单的看了一下实现的逻辑,wtfk这是啥呀!
下面就分析一下我的同事是怎么实现的,存在哪些问题。最后对限流做个小小的总结
问题代码
问题代码实现限流的方式是通过利用AOP对接标记注解的方法进行拦截,通过lua脚本来保存接口的执行次数。
注解类
public @interface RateLimiter {
//往令牌桶放入令牌的速率
double value() default Double.MAX_VALUE;
//获取令牌的超时时间
double limit() default Double.MAX_VALUE;
}
AOP核心实现
key值取得是当前时间秒数
RedisRateLimiter redisRateLimiter = signature.getMethod().getDeclaredAnnotation(RedisRateLimiter.class);
if(redisRateLimiter == null){
//正常执行方法,执行正常业务逻辑
return proceedingJoinPoint.proceed();
}
//获取注解上的参数,获取配置的速率
double value = redisRateLimiter.value();
double time = redisRateLimiter.limit();
//list设置lua的keys[1]
//取当前时间戳到单位秒
String key = "ip:"+ System.currentTimeMillis() / 1000;
List keyList = CollUtil.newArrayList(key);
//用户Mpa设置Lua 的ARGV[1]
//List argList = Lists.newArrayList(String.valueOf(value));
//调用脚本并执行
List result = stringRedisTemplate.execute(redisScript, keyList, String.valueOf(value),String.valueOf(time));
log.info("限流时间段内访问第:{} 次", result.toString());
//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 秒内累计请求次数
double value = redisRateLimiter.value();
//key过期时间
double time = redisRateLimiter.limit();
3.lua脚本获取时间参数redis.call("expire", key,ARGV[1])
总结
在实际开发中遇到要限流的需求,我们应该首先考虑了解限流算法实现方式有哪些,业务场景适合哪种算法,最后是考虑用什么组件方便。
常见的限流算法有以下几种:令牌桶算法 (Token Bucket)漏桶算法 (Leaky Bucket)计数器算法 (Counter)/固定窗口滑动窗口算法 (Sliding Window)基于时间的限流
在 Java 中,常用的限流组件主要有以下几种,它们各自实现限流的原理和机制略有不同:
常见的限流中间件Guava RateLimiterResilience4j RateLimiterSpring Cloud Gateway RateLimiterHystrix (虽然已不再维护)Bucket4jRedis 限流