天天看點

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配置限流

gateway基于令牌桶算法限流