计数器法
计数器法就是在固定的时间段内固定请求的访问数量,如果在某时间段内超出了设置的数量,那么就会触发接口限流,实现也很简单:
下面是接口代码:
@Controller
public class LimitController {
private Long timestamp = System.currentTimeMillis();
private int reqCount = 0;
@RequestMapping("/testCounter")
@ResponseBody
public String test1(){
Long interval = 60 * 1000L; // 60s
int limit = 5; //请求次数限制5次
long now = System.currentTimeMillis();//获得现在时间
if(now < timestamp + interval){
if(reqCount >= limit){
System.out.println(reqCount + ":error");
reqCount++;
return "error";//如果接口访问量在规定时间内超出限制,则返回错误信息
}
reqCount++;
System.out.println(reqCount + ":ok");
return "ok";//访问成功
}else{
timestamp = now;
reqCount = 1;
System.out.println(reqCount + ":ok");
return "ok";//发现时限已过,刷新时限,访问成功
}
}
}
计数器法的漏洞
计数器法有一个巨大的漏洞,那就攻击者很可能利用计数器刷新的时间点对接口连续进行两次大请求量的访问,例如:
那么结果可想而知,如果我们的服务器很脆弱,那么下场一定会很惨。
所以就有了下一种方法。
滑动窗口法
该方法是在计数器法上进行了一些优化,使攻击者无法在较短时间内突破服务器的请求数量限制。
请看原理:
其中窗口是在不断地滑动的,也就是说在这可变的一分钟内只有5次请求可以被处理,实现了真正的接口限流。
代码:
@Controller
public class WindowController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
int period = 60;//滑动窗口的宽度
int maxCount = 5;//最大次数
String userId = "yuan";
String actionKey = "view";
@RequestMapping("/testWindow")
@ResponseBody
public String test(){
String key = key(userId , actionKey);
//生成key
long now = System.currentTimeMillis();
//获得当前时间
stringRedisTemplate.opsForZSet().removeRangeByScore(key ,0,now -(period * 1000L));
//移除已经淘汰的键
Long aLong = stringRedisTemplate.opsForZSet().zCard(key);
System.out.println(aLong);
//统计现在存在的键
if(aLong!=null&&aLong>=maxCount){
return "error";
//限流发生
}
stringRedisTemplate.opsForZSet().add(key , String.valueOf(now), now);
//添加新键
return "ok";
}
static String key(String userId, String actionKey) {
return String.format("limit:%s:%s", userId, actionKey);
}
}
漏桶算法
漏桶算法顾名思义,就是用一个相当于漏斗的容器接收请求,然后以恒定的速率处理请求。
下面请看代码:
@Controller
public class LeakyBucketController {
private final int capacity = 10; // 桶的容量
private int water = 0; //当前水量
private int rate = 4; //水流速度
private long lastTime = System.currentTimeMillis();
@RequestMapping("/testBucket")
@ResponseBody
public String test(){
long now = System.currentTimeMillis();
//获得当前时间
water = Math.max(0,((int)(water - (now - lastTime)*rate / 1000)));
System.out.println(water);
lastTime = now;
//计算当前水量(当前水量减去从开始到现在流出的水量)即为当前水量,如果小于0,则赋为0
if(water>=capacity){
water = 10;//桶满溢出,将多余请求删去,在实际操作中可以用集合存储请求数据
return "fail";//返回失败
}else{
water++;
return "ok";//成功
}
}
}
令牌桶算法
令牌桶相较于漏桶算法,可以处理高并发的请求问题。
原理是令牌桶会以恒定的速率生成令牌,直到把桶装满为止,然后每个请求都需要携带一个令牌才可以被处理,否则就被销毁或者堆积。
令牌桶中令牌耗尽时(令牌生成速率小于使用速率):
下面是代码:
@Controller
public class TokenBucketController {
long lastTime = System.currentTimeMillis();
int capacity = 10;//令牌桶的容量
int rate = 4;//每秒钟生成4个令牌
int reqCount = 0;//请求数量
@RequestMapping("/testToken")
@ResponseBody
public String test(){
long now = System.currentTimeMillis();//获取现在的时间
reqCount++;
if(capacity<10){
capacity = Math.min(10 , (int) (capacity+(now - lastTime)*rate / 1000));
//每秒钟生成4个令牌,如果生成的令牌多余桶的容量,则直接将桶装满
lastTime = now;
//设置生成令牌的时间
}
System.out.println(reqCount+" " +capacity);
if(reqCount>capacity){
reqCount = reqCount - capacity;//令牌使用完
capacity = 0;
return "fail";//请求堵塞
}else{
capacity = capacity - reqCount ;
reqCount = 0;//处理请求
return "ok";
}
}
}
以上就是实现接口限流的四种方法,除了第一种算法,其他三种都是很优秀的实现接口限流的方法,没有优劣,只有合适不合适。可以根据项目体制选择合适的算法。