Java 介面限流
阿新 • • 發佈:2018-11-08
目錄:
- 限流原理
- 知識點
- 具體實現
- 結語
內容:
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; } }
以上程式碼要注意的點:
- Guava 用到了快取,感興趣的同學,可以自己深入學習一下;
- RateLimiter.create(2) 意思就是每秒生成兩個令牌,如果改為 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);
}
}
以上程式碼要注意的點:
- addPathPatterns 裡面的內容,就是要攔截的介面;
- 要攔截多個地方,可以用逗號隔開,比如:addPathPatterns("/api/v1/**", "/api/v2/**") 。
4、結語
如果我的部落格你看到了這裡,我想說明一下,我一般會在開頭就先寫實現的具體程式碼,而在最後進行總結。
之前在網上搜集過一些資料,還有用到自定義註解的,可以參考:https://blog.csdn.net/u013476435/article/details/82180663。而我沒有用的原因是:那些註解都用到了 aop,大部分在超流了以後,會通過拋異常的形式來處理,但我想要的時候通過返回給使用者一個“請求太頻繁”的提示,來達到目的。
當然,還沒有考慮到就是惡意攻擊。那就得再另起一篇來說明了,比如:增加 IP 黑名單等等。但就我們公司目前的業務,暫時是通過手動配置 IP 黑名單來處理的,還沒有在程式中限制。關於這塊,以後如果有用到的話,我會進行補充。在這寫出來,也是給以後的自己提個醒!