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