常见限流模式有:控制并发和控制速率。控制并发是限制并发的总数量(比如数据库连接池、线程池等),控制速率是限制并发访问的速率(如nginx的limit_conn模块,用来限制瞬时并发连接数)。另外还可以限制单位时间窗口内的请求数量(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率)。其他限流还可以限制远程接口调用速率、限制MQ的消费速率,根据网络连接数、网络流量、CPU或内存负载等来限流等。
在实际应用中可以通过信号量机制(Semaphore,线程安全:无名信号量,进程安全:有名信号量、V信号量)来实现。Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
例如:
public class SemaphoreService {
private final Semaphore permit = new Semaphore(10, true);
public void process(){
try{
permit.acquire();
//业务逻辑处理
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
permit.release();
}
}
}
Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore可以用tryAcquire()方法尝试获取许可证。
漏桶(Leaky Bucket)算法思路:水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。示意图如下:
这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
令牌桶算法原理:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法通过发放令牌,根据令牌的rate频率做请求频率限制,容量限制等。当需要提高速率,则按需提高放入桶中的令牌的速率即可。示意图如下:
每过1/r秒桶中增加一个令牌;桶中最多存放b个令牌,如果桶满了,新放入的令牌会被丢弃;当一个n字节的数据包到达时,消耗n个令牌,然后发送该数据包;如果桶中可用令牌小于n,则该数据包将被缓存或丢弃。
Google开源工具包Guava提供的限流工具类RateLimiter来实现控制速率,该类基于令牌桶算法来完成限流,非常易于使用,而且非常高效。java 测试样例:
public void testRateLimiter() {
RateLimiter limiter = RateLimiter.create(1);
for(int i = 1; i < 10; i = i + 2 ) {
double waitTime = limiter.acquire(i);
System.out.println("cutTime=" + System.currentTimeMillis() + " acq:" + i + " waitTime:" + waitTime);
}
}
输出结果:
cutTime=1535439657427 acq:1 waitTime:0.0
cutTime=1535439658431 acq:3 waitTime:0.997045
cutTime=1535439661429 acq:5 waitTime:2.993028
cutTime=1535439666426 acq:7 waitTime:4.995625
cutTime=1535439673426 acq:9 waitTime:6.999223
RateLimiter.create(1)用来创建一个限流器,参数代表每秒生成的令牌数,通过limiter.acquire(i);来以阻塞的方式获取令牌,当然也可以通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌,如果超timeout为0,则代表非阻塞,获取不到立即返回。RateLimiter通过限制后面请求的等待时间,来支持一定程度的突发请求(预消费)。RateLimiter适用于单体应用,且RateLimiter不保证公平性访问。
考虑在分布式应用中限定所有应用的单位时间内的总请求量,可以基于Redis + 拦截器实现。假设单位时间为1分钟,设置Redis key 有效时间为60s, 如果key不存在则设置有效时间和初始化计数为1, 当key存在时则只新增1,当新增总数超过限制阈值则拒绝请求,注意这里需要保证设置有效期和初始化计数为1时的原子性。
注解+拦截器实现
自定义注解:
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
int limit() default 5;
int sec() default 5;
}
拦截器:
public class AccessLimitInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (!method.isAnnotationPresent(AccessLimit.class)) {
return true;
}
AccessLimit accessLimit = method.getAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
int limit = accessLimit.limit();
int sec = accessLimit.sec();
String key = IPUtil.getIpAddr(request) + request.getRequestURI();
Integer maxLimit = redisTemplate.opsForValue().get(key);
if (maxLimit == null) {
redisTemplate.opsForValue().set(key, 1, sec, TimeUnit.SECONDS); //set时一定要加过期时间
} else if (maxLimit < limit) {
redisTemplate.opsForValue().set(key, maxLimit + 1, sec, TimeUnit.SECONDS);
} else {
output(response, "请求超过限制!");
return false;
}
}
return true;
}
public void output(HttpServletResponse response, String msg) throws IOException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = null;
try {
outputStream = response.getOutputStream();
outputStream.write(msg.getBytes("UTF-8"));
} catch (IOException e) {
e.printStackTrace();
} finally {
outputStream.flush();
outputStream.close();
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
controller:
@Controller
@RequestMapping("/activity")
public class AopController {
@ResponseBody
@RequestMapping("/test")
@AccessLimit(limit = 4,sec = 10) //加上自定义注解即可
public String test (HttpServletRequest request,@RequestParam(value = "username",required = false) String userName){
//TODO somethings……
return "hello world !";
}
}
配置文件:
/*springmvc的配置文件中加入自定义拦截器*/
主要介绍nginx 限流,采用漏桶算法。可以根据客户端请求IP,限制其访问频率。
用limit_req模块来限制基于IP请求的访问频率:
http://nginx.org/en/docs/http/ngx_http_limit_req_module.html
也可以用tengine中的增强版:
http://tengine.taobao.org/document_cn/http_limit_req_cn.html
1. 并发数和连接数控制的配置:
nginx http配置:
#请求数量控制,每秒20个
limit_req_zone $binary_remote_addr zone=one:10m rate=20r/s;
#并发限制30个
limit_conn_zone $binary_remote_addr zone=addr:10m;
server块配置
limit_req zone=one burst=5;
limit_conn addr 30;
2. ngx_http_limit_conn_module 限制单个IP的连接数:
ngx_http_limit_conn_module模块可以按照定义的键限定每个键值的连接数。可以设定单一 IP 来源的连接数。并不是所有的连接都会被模块计数;只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。
http {
limit_conn_zone $binary_remote_addr zone=addr:10m;
...
server {
...
location /download/ {
limit_conn addr 1;
}
参考:https://blog.csdn.net/zrg523/article/details/82185088