一、概况
限流防刷我认为有两种,一种是限制外层的请求到达服务器的数量,一种是服务器上的业务层限制用户调用接口的次数。
下面我将分别介绍这两者的具体实现。
二、业务上做接口防刷
1.实现原理
借助redis,key为用户id+请求的url,value为请求次数,过期时间根据业务情况设置。有漏洞,详情见下面的计数器算法说明。
2.具体实现
SpringBoot项目中,自定义@AccessLimit(limit = a, timeScope = b)注解,代表在b秒内最多请求a次,在Controller方法上面声明该注解,在写一个拦截器对每个Controller方法进行拦截,取出@AccessLimit中的值(如果有声明的话),从Redis中取当前用户调用当前RequestURI次数,进行判断即可。
具体代码:
AccessLimit:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 基于注解的调用次数限制 */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AccessLimit { /** * 请求限制数 * @return */ int limit(); /** * 时间范围 * @return */ int timeScope(); }
拦截器 LimitInterceptor:
@Component public class LimitInterceptor implements HandlerInterceptor { @Autowired RedisUtil redisUtil; private static final String PATH_LIMIT_REACHED = "调用太过频繁,请稍后再试"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod hm = (HandlerMethod) handler; // 拿到注解的内容 AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class); if (accessLimit == null) { // 不需要限流验证 return true; } else { // 需要限流验证 int limit = accessLimit.limit(); int timeScope = accessLimit.timeScope(); // 次数校验(借助redis实现基于用户的限流验证) JwtPlayload userInfo = (JwtPlayload) request.getAttribute("userInfo"); String redisKey = RedisPre.LIMITPRE+userInfo.getUserId()+request.getRequestURI(); Integer currentCount = (Integer)redisUtil.get(redisKey); if (currentCount!=null) { if (currentCount < limit) { redisUtil.incr(redisKey, 1); } else { // 访问过于频繁 throw new FlashSaleException(PATH_LIMIT_REACHED); } } else { redisUtil.set(redisKey, 1, timeScope); } } } return true; } }
配置类 WebConfig:
@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Autowired private LimitInterceptor limitInterceptor; /** * 注册拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(limitInterceptor).addPathPatterns("/**"); } }
Controller:
@AccessLimit(limit = 10, timeScope = 60) // 限制60秒内只能请求10次 public void helloWorld(){ return "Hello,World"; }
三、使用Nginx做请求流量限制
1.实现原理
计数器:
设置一个计数器counter,其有效时间为1分钟(即每分钟计数器会被重置为0),每当一个请求过来的时候,counter就加1,如果counter的值大于100,就说明请求数过多;
这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题。限制请求数为100,如果当前请求数为1,同时过来200个请求,查询的时候都发现可以请求,然而这一时间段内请求数已经达到200。
令牌桶算法:
令牌桶是一个存放固定容量令牌的桶,按照固定速率r往桶里添加令牌;桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃;
当一个请求达到时,会尝试从桶中获取令牌;如果有,则继续处理请求;如果没有则排队等待或者直接丢弃;
漏桶算法:
如下图所示,有一个固定容量的漏桶,按照常量固定速率流出水滴;如果桶是空的,则不会流出水滴;流入到漏桶的水流速度是随意的;如果流入的水超出了桶的容量,则流入的水会溢出(被丢弃);
可以发现,漏桶算法的流出速率恒定或者为0,而令牌桶算法的流出速率却有可能大于r;
2.具体实现
对Nginx进行配置: eg:
geo $limited { # the variable created is $limited
default 1;
127.0.0.1/32 0;
}
map $limited $limit {
1 $binary_remote_addr;
0 "";
}
limit_req_zone $binary_remote_addr zone=req_one:20m rate=12r/s; #对请求速率进行限制
limit_conn_zone $binary_remote_addr zone=addr:10m;
#对每个ip连接数量进行限制
limit_conn_zone $server_name zone=perserver:20m;
#对每个服务的连接数量进行限制
server{
listen 80;
location / {
proxy_pass http://13.11.123.31:80;
limit_req zone=req_one burst=100 nodelay;
limit_conn addr 15;
}
}
具体配置参考: