gateway基于令牌桶算法限流

1.限流过滤器

定义redis局部限流过滤器级别一定要最高的;如果级别比较低,限流过滤器放在最后那前面的操作提前下单,验证码什么的,前面都过来,结果都被限流给干掉了提前下单的处理那就没有意义了

@Component
public class RedistLimiterFilter implements GatewayFilter, Ordered {

    private Map<String, TokenTong> tongMap=new ConcurrentHashMap<>();
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //根据访问的url限流
        String url = exchange.getRequest().getPath().value();
        //生成令牌桶
        //每个url对应一个桶
        //一个url只有第一次需要创建桶,否则直接获取对应的桶即可,computeIfAbsent如果不存在put进去,如果存在直接拿
        TokenTong tokenTong = tongMap.computeIfAbsent(url,s-> new TokenTong(url, 10, 1));

        //从桶中获取令牌
//        double wait=tokenTong.reserveToken(1);
//        System.out.println("请求获取到令牌,等待时间为:"+wait);

        //放行
       // return chain.filter(exchange);

        //拿不到直接拒绝
        boolean b = tokenTong.tryReserveToken(1);
        if (b){
            //已经获得过的令牌
            return chain.filter(exchange);
        }

        //未获得的令牌
        //回写的对象
        ResultData resultData = new ResultData().setCode(ResultData.Code.MUSTEQUEST).setMsg("服务器繁忙");

        ServerHttpResponse response = exchange.getResponse();
        //将回写的数据转换成databuffer对象
        DataBuffer wrap = response.bufferFactory()
                .wrap(JSON.toJSONString(resultData).getBytes());
        //设置响应头告诉客户端,返回的是一个json
        response.getHeaders().put("Content-Type", Collections.singletonList("application/json"));
        return response.writeWith(Mono.just(wrap));

    }

    @Override
    public int getOrder() {
        return -1000;
    }
}

2.令牌桶的规则

2.1:这个类不能被spring扫描,一个TokenTong 对象
就是一个独立令牌桶,一旦被spring扫描,就变成单列的了,就算这个桶搞成多列的你也没办法初始化这个桶,我们初始化这个桶的最大的容量,一旦交给spring,spring是不管这些东西的
2.2:定义3个方法
reserveToken方法:(要不要预支)
尝试获取token的令牌,返回值表示获取这些令牌,需要等待的时间,如果返回0,无需等待,这就意味着当前没有预支,当前这个令牌桶是足够的,如果返回是大于0的,你得等待时间才能去继续你的请求。
tryReserveToken方法:
尝试预约token令牌,如果timeout时间范围内,可以预约到,就返回true,需要等待预约的时间,如果发现timeout时间范围内,没办法预约到当前令牌,直接返回false,表示令牌获取失败,无需等待
重载tryReserveToken方法:(意思就是如果超时时间等于0我就不会去等,我就能够知道能不能直接拿到会怎么样,如果不能拿到会怎么样)
直接获取指定数量的令牌,如果可以直接拿到,返回true,如果不能直接拿到,返回false
2.3:手动获得redis对象,为什么要用构造器而不能用静态代码块因为这样的话key、hasToken这些变量都是空的,赋值不上,这和创建对象的顺序有关;父类构造->默认初始化本类的非静态变量->收到初始化本类的非静态变量(直接赋值,非静态代码块 上 ->下) ->构造方法

/**
 * 令牌桶 
 * /
@Data
@Accessors(chain = true)
public class TokenTong {
    //获得令牌的脚本
    String getToken ="--令牌桶的key\n" +
            "local key = 'tokentong_'..KEYS[1]\n" +
            "--需要多少令牌\n" +
            "local getTokens = tonumber(ARGV[1])\n" +
            "\n" +
            "--获得令牌桶中的参数\n" +
            "--令牌桶中拥有的令牌\n" +
            "local hasToken = tonumber(redis.call('hget', key, 'hasToken'))\n" +
            "--令牌桶的最大令牌数\n" +
            "local maxToken = tonumber(redis.call('hget', key, 'maxToken'))\n" +
            "--每秒产生的令牌数\n" +
            "local tokensSec = tonumber(redis.call('hget', key, 'tokensSec'))\n" +
            "--下一次可以计算生成令牌的时间(微秒)\n" +
            "local nextTimes = tonumber(redis.call('hget', key, 'nextTimes'))\n" +
            "\n" +
            "--进行一些参数计算\n" +
            "--计算多久产生一个令牌(微秒)\n" +
            "local oneTokenTimes = 1000000/tokensSec\n" +
            "\n" +
            "--获取当前时间\n" +
            "local now = redis.call('time')\n" +
            "--计算当前微秒的时间戳\n" +
            "local nowTimes = tonumber(now[1]) * 1000000 + tonumber(now[2])\n" +
            "\n" +
            "--生成令牌\n" +
            "if nowTimes > nextTimes then\n" +
            "   --计算生成的令牌数\n" +
            "   local createTokens = (nowTimes - nextTimes) / oneTokenTimes\n" +
            "   --计算拥有的令牌数\n" +
            "   hasToken = math.min(createTokens + hasToken, maxToken)\n" +
            "   --更新下一次可以计算令牌的时间\n" +
            "   nextTimes = nowTimes\n" +
            "end\n" +
            "\n" +
            "--获取令牌\n" +
            "--当前能够拿到的令牌数量\n" +
            "local canTokens = math.min(getTokens, hasToken)\n" +
            "--需要预支的令牌数量\n" +
            "local reserveTokens = getTokens - canTokens\n" +
            "--根据预支的令牌数,计算需要预支多少时间(微秒)\n" +
            "local reserveTimes = reserveTokens * oneTokenTimes\n" +
            "--更新下一次可以计算令牌的时间\n" +
            "nextTimes = nextTimes + reserveTimes\n" +
            "--更新当前剩余的令牌\n" +
            "hasToken = hasToken - canTokens\n" +
            "\n" +
            "--更新redis\n" +
            "redis.call('hmset', key, 'hasToken', hasToken, 'nextTimes', nextTimes)\n" +
            "\n" +
            "--返回本次获取令牌需要等待的时间\n" +
            "return math.max(nextTimes - nowTimes, 0)";

//    @Autowired
//    private SpringUtil springUtil;
//当前的ben不是spring注入的所以不能注入,我们要主动去spring拿
//    @Autowired
    private StringRedisTemplate redisTemplate;
    //桶的名称
    private  String key;

    //当前拥有的令牌数量
    private int hasToken;

    //最大的令牌数量
    private int maxToken;

    //每秒生成多少令牌-(决定了令牌的生成速率)
    private int tokensSec;

    //这个构造器为什么没有当前拥有的令牌数量因为这个实时的,知道他最大的令牌数量就可以了
    public TokenTong(String key,int maxToken, int tokensSec) {
        this.key = key;
        this.maxToken = maxToken;
        this.tokensSec = tokensSec;
        this.redisTemplate=SpringUtil.getBean(StringRedisTemplate.class);
        //不能这么写this.redisTemplate=SpringUtil.getBean(redisTemplate.class); 因为redisTemplate是空的得用类名

        //初始化令牌桶
        init();
    }

    //这里为什么不用静态代码块呢?因为这样的话key、hasToken这些变量都是空的,赋值不上,这和创建对象的顺序有关
    //父类构造->默认初始化本类的非静态变量->收到初始化本类的非静态变量(直接赋值,非静态代码块 上 ->下) ->构造方法
//    {
//        this.redisTemplate=SpringUtil.getBean(StringRedisTemplate.class);
//    }

    /**
     * ------------令牌桶的操作方法------
     */
    //初始化令牌桶-redis中hash结构
    public void init(){
        Map<String,String> values=new HashMap<>();
        values.put("hasToken",hasToken+""); //当前令牌桶的数量
        values.put("maxToken",maxToken+"");  //最大令牌数量
        values.put("tokensSec",tokensSec+""); //每秒产生令牌的数量
        values.put("nextTimes", TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())+"");//下一次可以计算令牌的时间
        redisTemplate.opsForHash().putAll("tokentong_"+key,values);
    }
    
   public double reserveToken(int tokens){

        //执行脚本 ,返回的是Long类型,因为脚本是两个微秒数相减所以是Long
       Long execute = redisTemplate.execute(
               new DefaultRedisScript<Long>(getToken, long.class),
               Collections.singletonList(key),
               tokens + "");
       if (execute>0){
           //等待时间
           try {
               Thread.sleep(execute/1000);
           }catch (InterruptedException e){
               e.printStackTrace();
           }
       }
       return execute;
   }
    public boolean tryReserveToken(int tokens,int timeout,TimeUnit unit){
        Long execute = redisTemplate.execute(
                new DefaultRedisScript<Long>(getToken, long.class),
                Collections.singletonList(key),
                tokens + "", unit.toMicros(timeout)+""
        );
        if (execute ==-1){
            return  false;
        }else if (execute >0){
            //需要等待
            try {
                Thread.sleep(execute/1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        //可以获得令牌数
       return true;
    }
    public boolean tryReserveToken(int tokens){
     return tryReserveToken(tokens,0,TimeUnit.MICROSECONDS);
    }
}

3.因为TokenTong不能被spring扫描,导致当前的ben不是spring注入的所以不能注入 private StringRedisTemplate redisTemplate,但是我们可以主动去spring拿
他的意思是:spring扫到了SpringUtil 这个bean只要你实现了BeanFactoryAware 这个接口,他就会自动调里面的bean工程,他就会把bean工厂设置为0当然我们得加上 this.beanFactory=beanFactory;
3.2 :beanFactory和getBean设置成静态的因为你不设置静态的在TokenTong类中你还是得注入 private SpringUtil springUtil才能拿到getBean如果你new的话BeanFactoryAware 这个实现就没有意义了,所以加上静态的自己用

@Component
public class SpringUtil implements BeanFactoryAware {

    private static BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
           this.beanFactory=beanFactory;
    }

    /**
     * 手动从spring容器获得元素
     * @param <T>
     */
    public static <T> T getBean(Class c){
    //把这个元素返回
        return (T) beanFactory.getBean(c);
    }
}

4.如果要用的话还得创建一个类RedisLimitFilterFctory,name就是限流的名字

@Component
public class RedisLimitFilterFctory extends AbstractGatewayFilterFactory {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public GatewayFilter apply(Object config) {
        return (GatewayFilter) redisTemplate;
    }

    @Override
    public String name(){
        return "redisLimiter";
    }

}

5.gateway配置限流
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值