API接口限流竟然如此簡(jiǎn)單
簡(jiǎn)介
API接口限流是一種流量控制技術(shù),其目的是通過設(shè)置規(guī)則來(lái)限制客戶端對(duì)API接口的調(diào)用速率或總量,從而避免因過載而導(dǎo)致的服務(wù)性能下降甚至崩潰。
API限流在各種系統(tǒng)上都會(huì)有廣泛的使用場(chǎng)景,本文介紹一種非常簡(jiǎn)單的實(shí)現(xiàn)API限流的方式。
為什么需要API接口限流?
- 防止惡意攻擊:通過限制請(qǐng)求速率,可以有效抵御DDoS等類型的攻擊。
- 優(yōu)化資源使用:合理分配有限的計(jì)算資源給所有用戶,避免單個(gè)用戶占用過多資源。
- 提升服務(wù)質(zhì)量:保持服務(wù)響應(yīng)時(shí)間在一個(gè)合理的范圍內(nèi),提高整體用戶體驗(yàn)。
令牌桶
常見的API限流策略有令牌桶等算法。
令牌桶算法是一種常用的流量控制和限流機(jī)制,它通過模擬一個(gè)存放“令牌”的桶來(lái)控制請(qǐng)求的速率。
這個(gè)算法的核心思想是:系統(tǒng)以恒定的速率向桶中添加令牌,而每個(gè)請(qǐng)求在被處理之前必須從桶中獲取一個(gè)令牌。如果桶中有足夠的令牌,則請(qǐng)求可以繼續(xù)執(zhí)行;如果沒有足夠的令牌(即桶為空),則請(qǐng)求要么等待直到有新的令牌產(chǎn)生,要么直接被拒絕。
實(shí)現(xiàn)API限流
這個(gè)算法很容易理解,但是要想手動(dòng)實(shí)現(xiàn)一個(gè)令牌桶算法,并不是一個(gè)容易的事情。
還需要考慮:時(shí)間精度、并發(fā)處理、存儲(chǔ)管理、可配置性等問題。
Redis是一個(gè)常用的非關(guān)系型數(shù)據(jù)庫(kù),非常適合用于緩存、實(shí)現(xiàn)限流等功能。本文介紹一個(gè)利用redis非常簡(jiǎn)單的實(shí)現(xiàn)限流的功能,采用 AOP + 注解 + Redisson 框架實(shí)現(xiàn)。
1.定義限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 限流key,支持使用Spring el表達(dá)式來(lái)動(dòng)態(tài)獲取方法上的參數(shù)值
* 格式類似于 #code.id #{#code}
*/
String key() default "";
/**
* 限流時(shí)間,單位秒
*/
int time() default 60;
/**
* 限流次數(shù)
*/
int count() default 100;
/**
* 限流類型
*/
LimitType limitType() default LimitType.DEFAULT;
/**
* 提示消息
*/
String message() default "服務(wù)器暫無(wú)資源處理新的請(qǐng)求,請(qǐng)稍后重試";
}
public enum LimitType {
/**
* 默認(rèn)策略全局限流
*/
DEFAULT,
/**
* 根據(jù)請(qǐng)求者IP進(jìn)行限流
*/
IP,
/**
* 實(shí)例限流(集群多后端實(shí)例)
*/
CLUSTER
}
2.注解切面
@Slf4j
@Aspect
@Order(1)
public class RateLimiterAspect {
private static final String LIMITER_KEY = "global:limiter:";
/**
* 定義spel表達(dá)式解析器
*/
private final ExpressionParser parser = new SpelExpressionParser();
/**
* 定義spel解析模版
*/
private final ParserContext parserContext = new TemplateParserContext();
/**
* 方法參數(shù)解析器
*/
private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
/**
* \@within(rateLimiter) 和 \@annotation(rateLimiter) 必須按照這個(gè)順序,才會(huì)優(yōu)先執(zhí)行方法上的注解
*/
@Before("@within(rateLimiter) || @annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
if (rateLimiter == null) {
// 如果方法上沒有,就從類上獲取注解
Class<?> targetClass = point.getTarget().getClass();
rateLimiter = targetClass.getAnnotation(RateLimiter.class);
if (rateLimiter == null) {
// 如果還是沒有獲取到注解,直接返回
return;
}
}
int time = rateLimiter.time();
int count = rateLimiter.count();
try {
String combineKey = getCombineKey(rateLimiter, point);
RateType rateType = RateType.OVERALL;
if (rateLimiter.limitType() == LimitType.CLUSTER) {
rateType = RateType.PER_CLIENT;
}
long number = RedisUtils.rateLimiter(combineKey, rateType, count, time);
if (number == -1) {
throw new RateLimiterException(rateLimiter.message());
}
log.debug("限制令牌 => {}, 剩余令牌 => {}, 緩存key => '{}'", count, number, combineKey);
} catch (Exception e) {
if (e instanceof RateLimiterException) {
throw e;
} else {
throw new RuntimeException("服務(wù)器限流異常,請(qǐng)稍候再試", e);
}
}
}
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
// 判斷 key 不為空 和 不是表達(dá)式
if (StringUtils.hasText(key) && key.contains("#")) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method targetMethod = signature.getMethod();
Object[] args = point.getArgs();
MethodBasedEvaluationContext context =
new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getBeanFactory()));
Expression expression;
if (key.startsWith(parserContext.getExpressionPrefix()) && key.endsWith(parserContext.getExpressionSuffix())) {
expression = parser.parseExpression(key, parserContext);
} else {
expression = parser.parseExpression(key);
}
key = expression.getValue(context, String.class);
}
StringBuilder str = new StringBuilder(LIMITER_KEY);
HttpServletRequest request = getRequest();
str.append(request.getRequestURI()).append(":");
if (rateLimiter.limitType() == LimitType.IP) {
// 獲取請(qǐng)求ip
str.append(ServletUtil.getClientIP(request)).append(":");
} else if (rateLimiter.limitType() == LimitType.CLUSTER) {
// 獲取客戶端實(shí)例id
str.append(RedisUtils.getClient().getId()).append(":");
}
return str.append(key).toString();
}
/**
* 獲取request
*/
private HttpServletRequest getRequest() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes.getRequest();
} catch (Exception e) {
return null;
}
}
}
3.Redisson 限流工具類
public class RedisUtils {
private static final RedissonClient CLIENT = SpringUtil.getBean(RedissonClient.class);
/**
* 限流
*
* @param key 限流key
* @param rateType 限流類型
* @param rate 速率
* @param rateInterval 速率間隔
* @return -1 表示失敗
*/
public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) {
RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
// 如果限流器存在
if (rateLimiter.isExists()) {
// 獲取上次限流的配置信息
RateLimiterConfig rateLimiterConfig = rateLimiter.getConfig();
// 如果rateLimiterConfig的配置跟我們注解上面的值不一致,說(shuō)明服務(wù)器重啟過,程序員又修改了限流的配置
if (TimeUnit.SECONDS.convert(rateLimiterConfig.getRateInterval(), TimeUnit.MILLISECONDS) != rateInterval || rateLimiterConfig.getRate() != rate) {
rateLimiter.delete();
rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS);
}
}
rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS);
if (rateLimiter.tryAcquire()) {
return rateLimiter.availablePermits();
} else {
return -1L;
}
}
/**
* 獲取客戶端實(shí)例
*/
public static RedissonClient getClient() {
return CLIENT;
}
}
4.捕獲異常
@Data
@EqualsAndHashCode(callSuper = true)
public class RateLimiterException extends RuntimeException {
/**
* 錯(cuò)誤提示
*/
private final String message;
public RateLimiterException(String message) {
this.message = message;
}
}
@Slf4j
@Order(1)
@RestControllerAdvice
public class LimiterExceptionHandler {
/**
* 限流異常
*/
@ExceptionHandler({RateLimiterException.class})
public Map<String, Object> handleRateLimiterException(RateLimiterException e, HttpServletRequest request) {
log.error("請(qǐng)求地址'{}', 限流異常'{}'", request.getRequestURI(), e.getMessage());
return result(e.getMessage());
}
private Map<String, Object> result(String msg) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("msg", msg);
return result;
}
}
到這里,已經(jīng)實(shí)現(xiàn)了一個(gè)完整的API接口限流功能。
可以將之進(jìn)一步封裝,作為一個(gè)springboot的starter,用于任意一個(gè)項(xiàng)目中。
小結(jié)
通過上述步驟,我們已經(jīng)成功實(shí)現(xiàn)了一個(gè)基于Redisson和Spring AOP的API接口限流功能。這個(gè)方案不僅簡(jiǎn)單易懂,而且非常靈活,可以通過注解輕松地應(yīng)用到任意方法上,并且支持多種限流策略(如全局限流、IP限流、集群限流等)。