基于AOP的限流处理的一些基本方法
1.一些基本的实现方法
1.1guava:令牌桶
private static final ConcurrentMap<String, com.google.common.util.concurrent.RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
基于guava包下的Ratelimiter,key就是方法名表示,RateLimiter有两个核心方法:.create(qps)<每秒的令牌数>,tryAcqueir()<>可以设置异步的等待时间,尝试多少时间未果后就返回失败
1.2redis:时间窗口
zset(有序集合)实现时间窗口,<类似于阻塞队列>
boolean limited = shouldLimited(key, max, timeout, timeUnit);
这个提取方法的参数意义:
- key,包含接口的特质信息
- max:最大访问数
- timeout,timeUnit过期时间
方法的逻辑转换
//把时间差转换成毫秒
long ttl = timeUnit.toMillis(timeout);
// 当前时间毫秒数
long now = Instant.now().toEpochMilli();
long expired = now - ttl;//处理之前的zset元素
Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + "");
这里的思维不是拿取令牌桶,而是通过当前时间获取超时时间范围里的总数:若超过,则限流:以下是lua脚本
local key = KEYS[1]-- 传入参数第一个键值
local now = tonumber(ARGV[1]) --当前时间戳
local ttl = tonumber(ARGV[2]) --时间差
local expired = tonumber(ARGV[3]) --过期时间戳
-- 超时时间
local max = tonumber(ARGV[4]) --限制最大个数
-- 1.清除过期的数据
redis.call('zremrangebyscore', key, 0, expired)-- 移除指定有序集合中,scope在0到expired之间的元素
-- 2.获取 zset 中的当前元素个数
local current = tonumber(redis.call('zcard', key)) --获zcard有序集合元素个数
local next = current + 1
if next > max then
-- 达到限流大小 返回 0
return 0;
else
--3. 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score],一个zset有多个值
redis.call("zadd", key, now, now)
--刷新(若没有则设置)过期时间
redis.call("pexpire", key, ttl)
-- 自动过期
return next
end
1.3Redssion
RedissonClient中的RateLimiter对象
RRateLimiter limter = redissonClient.getRateLimiter("limiter:"+key);
//前面是个数,后面是时间
limter.trySetRate(RateType.OVERALL, 1, 30, RateIntervalUnit.SECONDS);
return limter.tryAcquire(1);
// 每30秒产生一个令牌
2.反射与aop
以下,结合Redssion进行介绍
2.1注解类的获取
注解类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limiter {
String key() default "test";
int qps() default 1;
int time() default 1000;
RateIntervalUnit timeUnit() default RateIntervalUnit.SECONDS;
int tpye() default 1;
}
在aop中使用实例对象+实例对象的反射获取注解
Object target = point.getTarget();
//对象的实例
Object[] args = point.getArgs();
MethodSignature signature = (MethodSignature) point.getSignature();
Limiter annotation = target.getClass().getMethod(signature.getName(), signature.getParameterTypes())
.getAnnotation(Limiter.class);
或者是工具类
MethodSignature signature = (MethodSignature) point.getSignature();
Object[] args = point.getArgs();
Method method = signature.getMethod();
//签名中方法+工具类
Limiter annotation = AnnotationUtils.findAnnotation(method, Limiter.class);
2.2方法传参作为key
Object[] args = point.getArgs();
在这个列表里,存储着参数,可以作为key
String key = ObjectUtils.isEmpty(args) ? annotation.key() : annotation.key() + ":" + args[0];
但如果这是是一个自定义对象,我们可以通过反射调用对象的方法:例如id,name等特征属性
String propVal = (String) arg.getClass().getMethod(
//return "get" + first.toUpperCase() + last;
getterName(propName))
//getterName(propName)是对属性值的拼接,可以设置某些提取对象的某些属性
.invoke(arg);
3.枚举+策略模式优化注解
用tpye字段进行选择,枚举的value方法返回自身对象构成的数组,找到对应的策略实现类
public enum TpyeEnum {
Common(1,"普通接口限流",new Commonserviceimpl())
,Code(2,"验证码接口限流",new CodeServiceimpl())
;
private final int tpye;
private final String description;
@Getter
private final ExceptionService exceptionService;
TpyeEnum(int tpye, String description, ExceptionService exceptionService) {
this.tpye = tpye;
this.description = description;
this.exceptionService = exceptionService;
}
public static TpyeEnum fromCode(int tpye) {
return Arrays.stream(TpyeEnum.values())
.filter(tpyeEnum -> tpyeEnum.tpye == tpye)
.findFirst().
orElseThrow(()->new RuntimeException("没有找到对应的枚举"));
}
}
实现类调用方法
public interface ExceptionService {
void handleException();
}
调用
tpyeEnum.getExceptionService().handleException();
这样就能根据注解中不同的tpye执行不同策略