redis工具包开发——限流模块(滑动窗口、漏斗、令牌桶)的实现

限流模块主要是三种限流的算法+aop实现

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RedisBloomFilterRegistar.class, RedisLimiterRegistar.class})
public @interface EnableRedisAux {
    String[] bloomFilterPath() default "";
    boolean enableLimit() default false;
    boolean transaction() default false;

}

然后spring会加载@Import的类,被注入的类通过获取注解上的信息来确定是否启用切面

public class RedisLimiterRegistar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map attributes = importingClassMetadata
                .getAnnotationAttributes(EnableRedisAux.class.getCanonicalName());
        Boolean enableLimit = (Boolean) attributes.get("enableLimit");
        //如果开启限流,则扫描组件、初始化对应的限流器和切面
        if(enableLimit){
            ClassPathBeanDefinitionScanner scanConfigure =
                    new ClassPathBeanDefinitionScanner(registry, true);
            scanConfigure.scan("com.opensource.redisaux.limiter.autoconfigure");
        }
    }

}

然后到配置类,主要是加载三个脚本,并且把对应的限流器缓存起来并加载切面类

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisTemplate.class)
public class RedisLimiterAutoConfiguration {

    @Autowired
    @Qualifier(BloomFilterConsts.INNERTEMPLATE)
    private RedisTemplate redisTemplate;

    /**
     * 滑动窗口的lua脚本,步骤:
     * 1.记录当前时间戳
     * 2.把小于(当前时间戳-窗口大小得到的时间戳)的key删掉
     * 3.返回该窗口内的成员个数
     * @return
     */
    @Bean
    public DefaultRedisScript windowLimitScript() {
        DefaultRedisScript script = new DefaultRedisScript();
        script.setResultType(Boolean.class);
        script.setScriptText("redis.call('zadd',KEYS[1],ARGV[1],ARGV[1]) redis.call('zremrangebyscore',KEYS[1],0,ARGV[2]) return redis.call('zcard',KEYS[1]) <= tonumber(ARGV[3])");
        return script;
    }

    /**
     * 具体思想看lua脚本注释
     * @return
     */
    @Bean
    public DefaultRedisScript tokenLimitScript() {
        DefaultRedisScript script = new DefaultRedisScript();
        script.setResultType(Long.class);
        script.setLocation(new ClassPathResource("TokenRateLimit.lua"));
        return script;
    }
    /**
     * 具体思想看lua脚本注释
     * @return
     */
    @Bean
    public DefaultRedisScript funnelLimitScript() {
        DefaultRedisScript script = new DefaultRedisScript();
        script.setResultType(Boolean.class);
        script.setLocation(new ClassPathResource("FunnelRateLimit.lua"));
        return script;
    }
    /**
     * 切面
     * @return
     */
    @Bean
    public LimiterAspect limiterAspect(){
        Map map = new HashMap();
        map.put(BaseRateLimiter.WINDOW_LIMITER, new WindowRateLimiter(redisTemplate, windowLimitScript()));
        map.put(BaseRateLimiter.TOKEN_LIMITER, new TokenRateLimiter(redisTemplate, tokenLimitScript()));
        map.put(BaseRateLimiter.FUNNEL_LIMITER, new FunnelRateLimiter(redisTemplate, funnelLimitScript()));
        return new LimiterAspect(map);
    }



}

切面类,主要是查看当前方法的注解所对应的类型是哪个,然后去map里面找对应实体类,进行限流操作,操作不通过则调用同类的失败方法,默认不传原有方法的参数,如果启用了传输参数,请把原来方法的参数搬到失败方法那里,并且不可以同名

@SuppressWarnings("unchecked")
@Aspect
public class LimiterAspect  {


    private final Map rateLimiterMap;

    private final Map annotationMap;


    public LimiterAspect(Map rateLimiterMap
    ) {
        this.rateLimiterMap = rateLimiterMap;
        this.annotationMap = new ConcurrentHashMap();
    }


    @Pointcut("@annotation(com.opensource.redisaux.limiter.annonations.TokenLimiter)||@annotation(com.opensource.redisaux.limiter.annonations.WindowLimiter)||@annotation(com.opensource.redisaux.limiter.annonations.FunnelLimiter)")
    public void limitPoint() {

    }


    @Around("limitPoint()")
    public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        Class beanClass = proceedingJoinPoint.getTarget().getClass();
        //获取所在类名
        String targetName = beanClass.getName();
        //获取执行的方法
        Method method = signature.getMethod();
        String methodKey = CommonUtil.getMethodKey(targetName, method);
        //该注解用于获取对应限流器
        LimiterType baseLimiter = null;
        Annotation target = null;
        if ((target = annotationMap.get(methodKey)) == null) {
            //找出限流器并且把对应的注解存到map里面
            Annotation[] annotations = signature.getMethod().getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation.annotationType().isAnnotationPresent(LimiterType.class)) {
                    target = annotation;
                    annotationMap.put(methodKey, target);
                    break;
                }
            }
        }
        baseLimiter = target.annotationType().getAnnotation(LimiterType.class);
        BaseRateLimiter rateLimiter = rateLimiterMap.get(baseLimiter.mode());
        if (rateLimiter.canExecute(target, methodKey)) {
            return proceedingJoinPoint.proceed();
        } else {
            //否则执行失败逻辑
            Object bean =proceedingJoinPoint.getTarget();
            BaseRateLimiter.KeyInfoNode keyInfoNode = BaseRateLimiter.keyInfoMap.get(methodKey);
            String fallBackMethodStr = keyInfoNode.getFallBackMethod();
            if ("".equals(fallBackMethodStr)) {
                return "too much request";
            }

           Method fallBackMethod= keyInfoNode.isPassArgs()?
                   beanClass.getMethod(fallBackMethodStr,method.getParameterTypes()):
                   beanClass.getMethod(fallBackMethodStr);
            fallBackMethod.setAccessible(true);
           return keyInfoNode.isPassArgs()?fallBackMethod.invoke(bean,proceedingJoinPoint.getArgs()):fallBackMethod.invoke(bean);
        }
    }



}

然后到限流设计部分,四个注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface LimiterType {
    /**
     * 模式,用于去相应的map里面寻找对应的limiter
     * @return
     */
    int mode();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@LimiterType(mode = BaseRateLimiter.TOKEN_LIMITER)
public @interface TokenLimiter {

    /**
     * 令牌桶容量
     *
     * @return
     */
    double capacity();

    /**
     * 令牌生成速率
     *
     * @return
     */
    double rate();

    /**
     * 速率时间单位,默认秒
     *
     * @return
     */
    TimeUnit rateUnit() default TimeUnit.SECONDS;

    /**
     * 每次请求所需要的令牌数
     *
     * @return
     */
    double need();

    /**
     * 是否阻塞等待
     *
     * @return
     */
    boolean isAbort() default false;

    /**
     * 阻塞超时时间
     *
     * @return
     */
    int timeout() default -1;

    /**
     * 单位,默认毫秒
     *
     * @return
     */
    TimeUnit timeoutUnit() default TimeUnit.MILLISECONDS;

    String fallback() default "";

    boolean passArgs() default false;

}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@LimiterType(mode = BaseRateLimiter.FUNNEL_LIMITER)
public @interface FunnelLimiter {

    /**
     * 漏斗容量
     * @return
     */
    double capacity();
    /**
     *每秒漏出的速率
     * @return
     */
    double passRate();
    /**
     *时间单位
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     *每次请求所需加的水量
     * @return
     */
    double addWater();

    String fallback() default "";

    boolean passArgs() default false;

}

@LimiterType是为了确定当前方法需要的限流类型

其他的注解里面的信息用于后面业务逻辑处理

这里有一个抽象限流器,里面有一些公共信息map,和对应的方法,这个KeyInfoNode里的keyList是接口映射到redis的keyName,因为脚本执行传参是list,passArgs和fallBack就是失败后的方法信息,有一个判断是否能通过的方法

public abstract class BaseRateLimiter {
    public final static int WINDOW_LIMITER =1;
    public final static int TOKEN_LIMITER =2;
    public final static int FUNNEL_LIMITER=3;
    //存放的是keyNameList、是否传参,回调方法名
    public static Map keyInfoMap =new ConcurrentHashMap();



    
   static List getKey(String methodKey,String method,boolean passArgs){
        KeyInfoNode keyInfoNode;
        if((keyInfoNode= keyInfoMap.get(methodKey))==null){
            keyInfoNode= new KeyInfoNode();
            keyInfoNode.fallBackMethod=method;
            keyInfoNode.passArgs=passArgs;
            keyInfoNode.keyNameList= Collections.singletonList(methodKey);
            keyInfoMap.put(methodKey, keyInfoNode);
        }
        return keyInfoNode.getKeyNameList();
    }

    /**
     * 限流情况下,是否可以通过执行
     * @param redisLimiter
     * @param methodKey
     * @return
     */
    public   Boolean canExecute(Annotation redisLimiter, String methodKey){return null;};


    public static class KeyInfoNode{

        private  List keyNameList;
        private  boolean passArgs;
        private String fallBackMethod;


        public List getKeyNameList() {
            return keyNameList;
        }

        public boolean isPassArgs() {
            return passArgs;
        }

        public String getFallBackMethod() {
            return fallBackMethod;
        }
    }

}

看一下漏斗限流,这里通过aop拦截方法所获取的注解信息来确定限流方法的执行参数

public class FunnelRateLimiter extends BaseRateLimiter {
    private RedisTemplate redisTemplate;
    private DefaultRedisScript redisScript;


    public FunnelRateLimiter(RedisTemplate redisTemplate, DefaultRedisScript redisScript) {
        this.redisScript = redisScript;
        this.redisTemplate = redisTemplate;

    }

    @Override
    public Boolean canExecute(Annotation baseLimiter, String methodKey) {
        FunnelLimiter funnelLimiter = (FunnelLimiter) baseLimiter;
        TimeUnit timeUnit = funnelLimiter.timeUnit();
        double capacity = funnelLimiter.capacity();
        double need = funnelLimiter.addWater();
        double rate = funnelLimiter.passRate();
        long l = timeUnit.toMillis(1);
        double millRate = rate / l;
        String methodName=funnelLimiter.fallback();
        boolean passArgs=funnelLimiter.passArgs();
        List keyList = BaseRateLimiter.getKey(methodKey,methodName,passArgs);
        return (Boolean) redisTemplate.execute(redisScript, keyList, new Object[]{capacity, millRate, need, Double.valueOf(System.currentTimeMillis())});
    }


}

执行的脚本如下

通过redis的hash表来构造一个漏斗器对象,它的属性有,漏斗容量,漏水的速率,一次请求所加的水,最后一次请求的时间,当前的水量

当请求来时,根据上一次请求的时间和本次时间来计算这段时间所流出的水,然后设置当前的水量,再判断是否可以加水,如果可以的话,更新最后一次请求时间和当前水量,这种限流方式可以保证通过的请求量是稳定的,因为漏斗的单位时间通过的水量是恒定的。

--参数说明,key[1]为对应服务接口的信息,argv1为capacity,argv2为漏水速率,argv3为一次所需流出的水量,argv4为时间戳
local limitInfo = redis.call('hmget', KEYS[1], 'capacity', 'passRate', 'addWater','water', 'lastTs')
local capacity = limitInfo[1]
local passRate = limitInfo[2]
local addWater= limitInfo[3]
local water = limitInfo[4]
local lastTs = limitInfo[5]

--初始化漏斗
if capacity == false then
    capacity = tonumber(ARGV[1])
    passRate = tonumber(ARGV[2])
    --请求一次所要加的水量
    addWater=tonumber(ARGV[3])
    --当前水量
    water = 0
    lastTs = tonumber(ARGV[4])
    redis.call('hmset', KEYS[1], 'capacity', capacity, 'passRate', passRate,'addWater',addWater,'water', water, 'lastTs', lastTs)
    return true
else
    local nowTs = tonumber(ARGV[4])
    --计算距离上一次请求到现在的漏水量
    local waterPass = tonumber((nowTs - lastTs)* passRate)
    --计算当前水量,即执行漏水
    water=math.max(0,water-waterPass)
    --设置本次请求的时间
    lastTs = nowTs
    --判断是否可以加水
    addWater=tonumber(addWater)
    if capacity-water >= addWater then
        --加水
        water=water+addWater
        --更新当前水量和时间戳
        redis.call('hmset', KEYS[1], 'water', water, 'lastTs', lastTs)
        return true
    end
    return false
end

令牌桶限流

这里就直接贴脚本了,因为都是通过获取注解来给脚本作参数的,同样的,也是通过hash表来构造一个令牌桶对象,令牌桶数量、令牌生成速率、每次请求所需的令牌,上一次请求的时间

大概原理其实和漏斗很像,只不过请求所处的角色不同,这里可以看作是消费者,而漏斗算法那里请求可以看作是生产者。

过程:通过计算当前时间与上一次时间的时间段生成的令牌,然后是否够本次请求使用,并更新对应的信息,令牌桶限流由于定期生产令牌,所以可以响应瞬时的突发请求,比如某个时刻,令牌桶中有10个令牌,那么1秒甚至更短的时间内可以相应10个请求也不会出错,但由于漏斗限流是固定流出水量,当1s内发生10个请求,流速不一定跟的上,满了以后就只能拒绝服务了

--参数说明,key[1]为对应服务接口的信息,argv1为capacity,argv2为令牌生成速率,argv3为每次需要的令牌数,argv4为当前时间戳
local limitInfo = redis.call('hmget', KEYS[1], 'capacity', 'passRate', 'leftToken', 'lastTs')
local capacity = limitInfo[1]
local rate = limitInfo[2]
local leftToken = limitInfo[3]
local lastTs = limitInfo[4]

--初始化令牌桶
if capacity == false then
    capacity = tonumber(ARGV[1])
    rate = tonumber(ARGV[2])
    leftToken = tonumber(ARGV[1])
    lastTs = tonumber(ARGV[4])
    redis.call('hmset', KEYS[1], 'capacity', capacity, 'passRate', rate, 'leftToken', leftToken, 'lastTs', lastTs)
    return -1
else
    local nowTs = tonumber(ARGV[4])
    --计算距离上一次请求到现在生产令牌数
    local genTokenNum = tonumber((nowTs - lastTs)* rate)
    --计算该段时间的剩余令牌
    leftToken = genTokenNum + leftToken
    --设置剩余令牌
    leftToken = math.min(capacity, leftToken)
    --设置本次请求的时间
    lastTs = nowTs
    local need = tonumber(ARGV[3])
    --返回需要等待的毫秒数,-1则不用等待
    if leftToken >= need then
        --减去需要的令牌
        leftToken = leftToken - need
        --更新剩余空间和上一次的漏水时间戳
        redis.call('hmset', KEYS[1], 'leftToken', leftToken, 'lastTs', lastTs)
        return -1
    end
    return (need-leftToken)/rate
end

然后到滑动窗口,主要是用到了sorted set结构,ARGV[1]是当前请求时间戳,然后把超出窗口外的时间戳的请求数量都删除,返回当前的请求数是否小于滑动窗口单位时间通过的请求数

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@LimiterType(mode = BaseRateLimiter.WINDOW_LIMITER)
public @interface WindowLimiter {
    /**
     * 持续时间,窗口间隔
     * @return
     */
    int during();

    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 通过的请求数
     * @return
     */
    long value();

    String fallback() default "";

    boolean passArgs() default false;



}
redis.call('zadd',KEYS[1],ARGV[1],ARGV[1])
 redis.call('zremrangebyscore',KEYS[1],0,ARGV[2]) 
return redis.call('zcard',KEYS[1]) <= tonumber(ARGV[3])

目前的限流功能还有许多可以改进的地方,以后有时间就更新下

大概的实现思路就是这样,详情可以看github上的代码

你可能感兴趣的:(redis)