Rate Limit 限流

Submitted by Lizhe on Tue, 07/25/2017 - 12:22

如果你还记得12306网站那个著名的 "系统太忙请稍后再试"你一定遇到过限流

或者是jd的抢购活动的"服务器太忙请稍后再试" 都是限流的结果

限流的好处在于防止非预期的请求对系统压力过大而引起的系统崩溃

一般Web系统的访问限制都可以用容器本身来实现,比如tomcat就可以在connector上面配置connection数目的限制,servlet thread限制

不过如果不同的URL需要配置不同的限制, tomcat显然是做不到的

这个时候就需要实现自己的限制

 

常用的限流算法有两种   漏桶算法和令牌桶算法

 漏桶算法

请求会先进入一个漏桶里, 假设漏桶每分钟滴4滴水, 那么就有4个请求在这一分钟里被处理,如果水桶满了, 就意味着可以goto error page 了

9426

令牌桶算法

和漏桶正好相反, 它是生产可用的token然后放进桶里,每个请求到达以后去桶里拿token,如果拿不到就可以 goto error page 了

1931

 

这两种算法其实都是一个缓冲队列, 如果你要自己实现我推荐你使用rabbitmq之类的消息队列, 如果颗粒度太细的话而且只是实现简单计数功能也可以使用redis incr

如果不需要自己实现使用Google的guava也是一个不错的选择,guava 使用的是令牌桶算法

下面是一个使用springboot+guava的例子

/ratelimitsample/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>name.lizhe</groupId>
    <artifactId>ratelimitsample</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
           <groupId>javax.servlet</groupId>
           <artifactId>jstl</artifactId>
       </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>14.0.1</version>
        </dependency>
    </dependencies>
</project>

 

/ratelimitsample/src/main/webapp/WEB-INF/jsp/myerror.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>
</head>
<body>
error
</body>
</html>

/ratelimitsample/src/main/resources/application.properties

server.port=8080 
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

/ratelimitsample/src/main/java/name/lizhe/filter/RateLimitFilter.java

package name.lizhe.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.common.util.concurrent.RateLimiter;

public class RateLimitFilter implements Filter{
    
    private RateLimiter limiter = null;

    public void destroy() {
        
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        
        System.out.println("doFilter ... ");
        
        if(limiter.tryAcquire()) {
             System.out.println("Accessed sucessfully");
             chain.doFilter(request, response);
        } else {
            System.out.println("Accessed Failed");
             req.getRequestDispatcher("/myerrorjsp").forward(req,res);
        }
        
    }

    public void init(FilterConfig arg0) throws ServletException {
        limiter = RateLimiter.create(1); //10 request per second
    }

}
 

/ratelimitsample/src/main/java/name/lizhe/controller/Test.java

package name.lizhe.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Test {
    @RequestMapping("/ratelimit/test")
    public String test() {
        System.out.println("test...");
        return "accessed test";
    }

}

/ratelimitsample/src/main/java/name/lizhe/controller/ErrorController.java

package name.lizhe.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ErrorController {

    @RequestMapping("/myerrorjsp")
    public String errorJsp() {
        return "myerror";

    }

}

/ratelimitsample/src/main/java/name/lizhe/app/SpringRestApplication.java

package name.lizhe.app;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;

import name.lizhe.filter.RateLimitFilter;  
  

  
@SpringBootApplication  
@ComponentScan("name.lizhe")
public class SpringRestApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(SpringRestApplication.class, args);  
    }  
      
    @Bean  
    public FilterRegistrationBean  filterRegistrationBean() {  
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();  
        RateLimitFilter rateLimitFilter = new RateLimitFilter();  
        registrationBean.setFilter(rateLimitFilter);  
        List<String> urlPatterns = new ArrayList<String>();  
        urlPatterns.add("/ratelimit/*");  
        registrationBean.setUrlPatterns(urlPatterns);  
        return registrationBean;  
    }  
}  
 

完整的例子可以从https://github.com/zl86790/ratelimitsample.git下载

因为设置了1秒钟只能处理一个请求,所以连续刷新页面会得如下结果

446