概念
限流 限流的目的是通過對併發訪問/請求進行限速,或者對一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理
常用限流算法
常用的限流算法有兩種:漏桶算法和令牌桶算法
漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以一定的速度出水,當水流入速度過大會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。
對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。
令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。
控制併發數量
信號量Semaphore
Semaphore是一種基於計數的信號量。它可以設定一個閾值,基於此,多個線程競爭獲取許可信號,做完自己的申請後歸還,超過閾值後,線程申請許可信號將會被阻塞。
簡單的說:Semaphore(10)表示允許10個線程獲取許可證,也就是最大併發數是10。
控制訪問速率
限流工具類RateLimiter
Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法來完成限流,非常易於使用。
RateLimiter源碼分析
調用create接口時,實際實例化的爲SmoothBursty類
static final class SmoothBursty extends SmoothRateLimiter {
/** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */
final double maxBurstSeconds;
/**
* The currently stored permits.
* 當前存儲令牌數
*/
double storedPermits;
/**
* The maximum number of stored permits.
* 最大存儲令牌數
*/
double maxPermits;
/**
* The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
* per second has a stable interval of 200ms.
* 添加令牌時間間隔,可以理解成生成一個令牌需要的時間,這裏是微秒單位
*/
double stableIntervalMicros;
.......
}
RateLimiter 創建
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
acquire()方法
public double acquire(int permits) {
//計算獲取這些請求需要讓線程等待多長時間
long microsToWait = reserve(permits);
//讓線程阻塞microTowait微秒長的時間
stopwatch.sleepMicrosUninterruptibly(microsToWait);
//返回阻塞的時間
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
reserve()方法
final long reserve(int permits) {
//檢查permits是否合法
checkPermits(permits);
//保證線程安全
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
reserveEarliestAvailable()
storedPermitsToSpend爲桶中可以消費的令牌數,freshPermits爲還需要的(需要補充的)令牌數,根據該值計算需要等待的時間,追加並更新到nextFreeTicketMicros
//獲取requiredPermits個令牌,並返回需要等待到的時間點
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros);
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
double freshPermits = requiredPermits - storedPermitsToSpend;
//當 requiredPermits>storedPermits纔會有實際意義,這段代碼允許我們提前獲取令牌,但是這種情況會造成下一次令牌生成的時間推遲。有種預支工資的意思
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
//更新可消費的令牌
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
```
#### resync()
若當前時間晚於nextFreeTicketMicros,則計算該段時間內可以生成多少令牌,將生成的令牌加入令牌桶中並更新數據
```
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
//時間間隔內生成的新令牌
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
//更新最大令牌數量
storedPermits = min(maxPermits, storedPermits + newPermits);
//下次可以獲取的時間
nextFreeTicketMicros = nowMicros;
}
}
tryAcquire函數可以嘗試在timeout時間內獲取令牌,如果可以則掛起等待相應時間並返回true,否則立即返回false
canAcquire用於判斷timeout時間內是否可以獲取令牌。
應用
攔截器配置,可以統一配置所有請求的上限,也可以單獨對某個 url配置,該攔截器是基於 SpringMvc 的RequestMappingHandlerMapping獲取url 進行操作。
<bean id="requestLimitInterceptor" class="cn.fraudmetrix.creditcloud.app.intercepters.RequestLimitInterceptor">
<property name="globalRateLimiter" value="100" />
<property name="urlProperties">
<props>
<prop key="/creditcloud/test">100</prop>
</props>
</property>
</bean>
<!--攔截器配置-->
<mvc:interceptors>
<ref bean="requestLimitInterceptor" />
</mvc:interceptors>
RequestLimitInterceptor 攔截器
public class RequestLimitInterceptor implements HandlerInterceptor ,BeanPostProcessor{
private Logger logger = LoggerFactory.getLogger(RequestLimitInterceptor.class);
private Integer globalRateLimiter = 100;
private Map<PatternsRequestCondition, RateLimiter> urlRateMap;
private Properties urlProperties;
private UrlPathHelper urlPathHelper = new UrlPathHelper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (urlRateMap != null) {
String lookupPath = urlPathHelper.getLookupPathForRequest(request);
for (PatternsRequestCondition patternsRequestCondition : urlRateMap.keySet()) {
//使用spring DispatcherServlet的匹配器PatternsRequestCondition進行匹配
List<String> matches = patternsRequestCondition.getMatchingPatterns(lookupPath);
if (!matches.isEmpty()) {
if (urlRateMap.get(patternsRequestCondition).tryAcquire(1000, TimeUnit.MILLISECONDS)) {
logger.info(" 請求'{}'匹配到mathes {} ,成功獲取令牌,進入請求。" ,lookupPath ,Joiner.on(",").join(patternsRequestCondition.getPatterns()) );
} else {
logger.info( " 請求'{}'匹配到mathes {},超過限流速率,獲取令牌失敗。" ,lookupPath ,Joiner.on(",").join(patternsRequestCondition.getPatterns()));
return false;
}
}
}
}
return true;
}
@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 {
}
/**
* 限流的 URL與限流值的K/V 值
*
* @param urlProperties
*/
public void setUrlProperties(Properties urlProperties) {
this.urlProperties = urlProperties;
}
public void setGlobalRateLimiter(Integer globalRateLimiter) {
this.globalRateLimiter = globalRateLimiter;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(RequestMappingHandlerMapping.class.isAssignableFrom(bean.getClass())){
if(urlRateMap==null){
urlRateMap = new ConcurrentHashMap<>();
}
logger.info("we get all the controllers's methods and assign it to urlRateMap");
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)bean;
Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
for (RequestMappingInfo rmi : handlerMethods.keySet()) {
PatternsRequestCondition pc = rmi.getPatternsCondition();
urlRateMap.put(pc,RateLimiter.create(globalRateLimiter));
}
if(urlProperties!=null){
for(String urlPatterns :urlProperties.stringPropertyNames()){
String limit = urlProperties.getProperty(urlPatterns);
if(!limit.matches("^-?\\d+$"))
logger.error("the value {} for url patterns {} is not a number ,please check it ",limit,urlPatterns);
urlRateMap.put(new PatternsRequestCondition(urlPatterns), RateLimiter.create(Integer.parseInt(limit)));
}
}
}
return bean;
}
}
總結
RateLimiter通常用於限制訪問某些物理或邏輯資源的速率。這與jdk併發包中的Semaphore相反,它限制併發訪問的數量而不是速率(注意,併發和速率是密切相關的)。