1. 程式人生 > >Java 介面限流

Java 介面限流

目錄:

  1. 限流原理
  2. 知識點
  3. 具體實現
  4. 結語

 

內容:

1、限流原理 -- 令牌桶演算法 

令牌桶演算法的原理是系統會以一個恆定的速度(每秒生成一個令牌)往桶裡放入令牌。當有訪問者(針對於 IP)要訪問介面時,則需要先從桶裡獲取一個令牌,當桶裡沒有令牌可取時,則拒絕服務。 當桶滿時,新新增的令牌被丟棄或拒絕。

 

2、知識點

  • Springboot
  • Guava -- RateLimiter
  • Interceptor(攔截器)

 

3、具體實現

1)先寫一個限流 Service -- LoadingCacheService,程式碼如下:

@Service
public class LoadingCacheService {

    private final Logger logger = LoggerFactory.getLogger(LoadingCacheService.class);

    private LoadingCache<String, RateLimiter> ipRequestCaches = CacheBuilder.newBuilder()
            // 設定快取上限
            .maximumSize(10000)
            // 設定一分鐘物件沒有被讀/寫訪問則物件從記憶體中刪除
            .expireAfterAccess(1, TimeUnit.MINUTES)
            // CacheLoader 類實現自動載入
            .build(new CacheLoader<String, RateLimiter>() {
                @Override
                public RateLimiter load(String s) {
                    // 新的 IP 初始化 (限流每秒生成 2 個令牌)
                    return RateLimiter.create(2);
                }
            });

    public boolean hasToken(HttpServletRequest request) {
        try {
            String ip = this.getIPAddress(request);
            String url = request.getRequestURL().toString();
            String key = "req_limit_".concat(url).concat(ip);
            // 有則返回,沒有就新增後獲取
            RateLimiter limiter = ipRequestCaches.get(key);

            return limiter.tryAcquire();
        } catch (Exception e) {
            logger.error("獲取令牌異常:", e);
        }
        return false;
    }

    /**
     * 獲取當前網路 ip
     *
     * @param request HttpServletRequest
     * @return 真實的 ip 地址
     */
    private String getIPAddress(HttpServletRequest request) {
        String ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("x-forwarded-for");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
        }
        // 對於通過多個代理的情況,第一個 IP 為客戶端真實 IP,多個 IP 按照','分割
        // "***.***.***.***".length() = 15
        if (ipAddress != null && ipAddress.length() > 15) {
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }

}

以上程式碼要注意的點:

  1. Guava 用到了快取,感興趣的同學,可以自己深入學習一下;
  2. RateLimiter.create(2) 意思就是每秒生成兩個令牌,如果改為 3 ,就是每秒生成 3 個;
  3. 僅僅靠 request.getRemoteAddr() 有可能獲取不到使用者的真實 IP ,需要用 getIPAddress() 方法。

2)寫一個攔截器元件 -- RequestInterceptor

@Component
public class RequestInterceptor implements HandlerInterceptor {

    @Resource
    private LoadingCacheService loadingCacheService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws IOException {
        if (loadingCacheService.hasToken(request)) {
            return true;
        }
        outputError(response);
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) {

    }

    private void outputError(HttpServletResponse response) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(429);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(JSONObject.toJSONString(new ErrorRes("請求太頻繁!")).getBytes("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                outputStream.flush();
                outputStream.close();
            }
        }
    }

}

3)編寫攔截器配置類 -- InterceptorConfig,並註冊剛才編寫的攔截器 -- RequestInterceptor

@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {

    @Resource
    private RequestInterceptor requestInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestInterceptor).addPathPatterns("/api/v1/**");
        // 註冊
        super.addInterceptors(registry);
    }

}

以上程式碼要注意的點:

  1. addPathPatterns 裡面的內容,就是要攔截的介面;
  2. 要攔截多個地方,可以用逗號隔開,比如:addPathPatterns("/api/v1/**", "/api/v2/**") 。

 

4、結語

如果我的部落格你看到了這裡,我想說明一下,我一般會在開頭就先寫實現的具體程式碼,而在最後進行總結。

之前在網上搜集過一些資料,還有用到自定義註解的,可以參考:https://blog.csdn.net/u013476435/article/details/82180663。而我沒有用的原因是:那些註解都用到了 aop,大部分在超流了以後,會通過拋異常的形式來處理,但我想要的時候通過返回給使用者一個“請求太頻繁”的提示,來達到目的。

當然,還沒有考慮到就是惡意攻擊。那就得再另起一篇來說明了,比如:增加 IP 黑名單等等。但就我們公司目前的業務,暫時是通過手動配置 IP 黑名單來處理的,還沒有在程式中限制。關於這塊,以後如果有用到的話,我會進行補充。在這寫出來,也是給以後的自己提個醒!