1. 什么是限流
限流就是在用戶訪問(wèn)次數(shù)龐大時(shí),對(duì)系統(tǒng)資源的一種保護(hù)手段。高峰期,用戶可能對(duì)某個(gè)接口的訪問(wèn)頻率急劇升高,后端接口通常需要進(jìn)行DB操作,接口訪問(wèn)頻率升高,DB的IO次數(shù)就顯著增高,從而極大的影響整個(gè)系統(tǒng)的性能。如果不對(duì)用戶訪問(wèn)頻率進(jìn)行限制,高頻的訪問(wèn)容易打跨整個(gè)服務(wù)
2. 常見的限流策略
2.1 漏斗算法
我們想象一個(gè)漏斗,大口用于接收客戶端的請(qǐng)求,小口用于流出用戶的請(qǐng)求。漏斗能夠保證流出請(qǐng)求數(shù)量的穩(wěn)定。
2.2 令牌桶算法
令牌桶算法,每個(gè)請(qǐng)求想要通過(guò),就必須從令牌桶中取出一個(gè)令牌。否則無(wú)法通過(guò)。而令牌會(huì)內(nèi)部會(huì)維護(hù)每秒鐘產(chǎn)生的令牌的數(shù)量,使得每秒鐘能夠通過(guò)的請(qǐng)求數(shù)量得到控制
2.3 次數(shù)統(tǒng)計(jì)
次數(shù)統(tǒng)計(jì)的方式非常直接,每一次請(qǐng)求都進(jìn)行計(jì)數(shù),并統(tǒng)計(jì)時(shí)間戳。如果下一次請(qǐng)求攜帶的時(shí)間戳在一定的頻率內(nèi),進(jìn)行次數(shù)的累加。如果次數(shù)達(dá)到一定閾值,則拒絕后續(xù)請(qǐng)求。直到下一次請(qǐng)求時(shí)間戳大于初始時(shí)間戳,重置接口次數(shù)與時(shí)間戳
3. 令牌桶代碼編寫
令牌桶算法我們可以使用Google guava包下的封裝好的RateLimiter,緊緊抱住大爹大腿
另外,ip頻率限制是一個(gè)橫向邏輯,該功能應(yīng)該保護(hù)所有后端接口,因此我們可以采用Spring AOP增強(qiáng)所有后端接口
另外,我們需要對(duì)同一個(gè)用戶,對(duì)同一個(gè)接口訪問(wèn)次數(shù)進(jìn)行限流,這意味著我們需要限制的是——(用戶,接口)這樣的一對(duì)元組。用戶可以通過(guò)ip進(jìn)行限定,也就是說(shuō),后端是同一個(gè)ip針對(duì)同一個(gè)請(qǐng)求的訪問(wèn)進(jìn)行限流
因此我們需要為每一個(gè)這樣的(ip,method)使用令牌桶限流,(ip,method)-> RateLimiter。ip + method這一對(duì)元組唯一確定一個(gè)RateLimiter
我們可以采用Map緩存這樣的一一對(duì)應(yīng)的關(guān)系
But,HashMap顯然不適合,應(yīng)為HashMap不防并發(fā);另外ConcurrentHashMap也不合適,假如一個(gè)用戶發(fā)出一個(gè)請(qǐng)求后就下線了,那么這個(gè)key就會(huì)長(zhǎng)久的存活于內(nèi)存中,這極大的增加了內(nèi)存的壓力
因此我們采用Google的Cache
Google大爹提供的Cache功能極其強(qiáng)大,讀者可以自行閱讀下面文檔
/**
* A builder of {@link LoadingCache} and {@link Cache} instances having any combination of the
* following features:
*
* <ul>
* <li>automatic loading of entries into the cache
* <li>least-recently-used eviction when a maximum size is exceeded
* <li>time-based expiration of entries, measured since last access or last write
* <li>keys automatically wrapped in {@code WeakReference}
* <li>values automatically wrapped in {@code WeakReference} or {@code SoftReference}
* <li>notification of evicted (or otherwise removed) entries
* <li>accumulation of cache access statistics
* </ul>
* /
IpLimiterAspect.java
import com.fgbg.demo.utils.RequestUtils;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* 限制每個(gè)ip對(duì)同一個(gè)接口的訪問(wèn)頻率
*/
@Component
@Aspect
@Slf4j
@RestController
public class IpLimiterAspect {
@Autowired
private RequestUtils requestUtils;
// 每秒生成1個(gè)令牌, 同個(gè)ip訪問(wèn)同個(gè)接口的QPS為1
private final double PERMIT_PER_SECOND = 1;
// 創(chuàng)建本地緩存
private final Cache<String, RateLimiter> limiterCache = CacheBuilder.newBuilder().expireAfterAccess(5, TimeUnit.MINUTES).build();
@Around("execution(* com.fgbg.demo.controller..*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 構(gòu)造key
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String methodName = proceedingJoinPoint.getTarget().getClass().getName() + "." + methodSignature.getName();
String key = requestUtils.getCurrentIp() + "->" + methodName;
// 獲取key對(duì)應(yīng)的RateLimiter
RateLimiter rateLimiter = limiterCache.get(key, () -> RateLimiter.create(PERMIT_PER_SECOND));
if (! rateLimiter.tryAcquire()) {
// 如果不能立刻獲取令牌, 說(shuō)明訪問(wèn)速度大于1 次/s, 觸發(fā)限流
log.warn("訪問(wèn)過(guò)快, 觸發(fā)限流");
throw new RuntimeException("訪問(wèn)過(guò)快, 觸發(fā)限流");
}
log.info("接口放行...");
return proceedingJoinPoint.proceed();
}
}
RequestUtils.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class RequestUtils {
@Autowired
private HttpServletRequest httpServletRequest;
public String getCurrentIp() {
return httpServletRequest.getHeader("X-Real-IP");
}
}
4. 接口測(cè)試
接口測(cè)試這塊就比較隨意了,筆者這里采用apifox進(jìn)行接口測(cè)試。因?yàn)锳OP邏輯是增強(qiáng)所有接口,因此這里選擇了項(xiàng)目曾經(jīng)暴露出的一個(gè)查詢接口。點(diǎn)擊運(yùn)行,即可開始測(cè)試
5. 測(cè)試結(jié)果
2.6s,分別在0,1,2s開始時(shí),允許接口訪問(wèn)。10個(gè)請(qǐng)求中通過(guò)3個(gè),失敗7個(gè),QPS = 1,限流成功文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-854236.html
測(cè)試量達(dá)到40,QPS維持1,說(shuō)明代碼邏輯基本沒(méi)有問(wèn)題,Google yyds!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-854236.html
到了這里,關(guān)于【前后端的那些事】SpringBoot 基于內(nèi)存的ip訪問(wèn)頻率限制切面(RateLimiter)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!