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