AOP注解限流

173 阅读3分钟

基于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执行不同策略