简介
API接口限流是一种流量控制技术,其目的是通过设置规则来限制客户端对API接口的调用速率或总量,从而避免因过载而导致的服务性能下降甚至崩溃。
API限流在各种系统上都会有广泛的使用场景,本文介绍一种非常简单的实现API限流的方式。
为什么需要API接口限流?
- 防止恶意攻击:通过限制请求速率,可以有效抵御DDoS等类型的攻击。
- 优化资源使用:合理分配有限的计算资源给所有用户,避免单个用户占用过多资源。
- 提升服务质量:保持服务响应时间在一个合理的范围内,提高整体用户体验。
令牌桶
常见的API限流策略有令牌桶等算法。
令牌桶算法是一种常用的流量控制和限流机制,它通过模拟一个存放“令牌”的桶来控制请求的速率。
这个算法的核心思想是:系统以恒定的速率向桶中添加令牌,而每个请求在被处理之前必须从桶中获取一个令牌。如果桶中有足够的令牌,则请求可以继续执行;如果没有足够的令牌(即桶为空),则请求要么等待直到有新的令牌产生,要么直接被拒绝。
实现API限流
这个算法很容易理解,但是要想手动实现一个令牌桶算法,并不是一个容易的事情。
还需要考虑:时间精度、并发处理、存储管理、可配置性等问题。
Redis是一个常用的非关系型数据库,非常适合用于缓存、实现限流等功能。本文介绍一个利用redis非常简单的实现限流的功能,采用 AOP + 注解 + Redisson 框架实现。
1.定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 限流key,支持使用Spring el表达式来动态获取方法上的参数值
* 格式类似于 #code.id #{#code}
*/
String key() default "";
/**
* 限流时间,单位秒
*/
int time() default 60;
/**
* 限流次数
*/
int count() default 100;
/**
* 限流类型
*/
LimitType limitType() default LimitType.DEFAULT;
/**
* 提示消息
*/
String message() default "服务器暂无资源处理新的请求,请稍后重试";
}
public enum LimitType {
/**
* 默认策略全局限流
*/
DEFAULT,
/**
* 根据请求者IP进行限流
*/
IP,
/**
* 实例限流(集群多后端实例)
*/
CLUSTER
}
2.注解切面
@Slf4j
@Aspect
@Order(1)
public class RateLimiterAspect {
private static final String LIMITER_KEY = "global:limiter:";
/**
* 定义spel表达式解析器
*/
private final ExpressionParser parser = new SpelExpressionParser();
/**
* 定义spel解析模版
*/
private final ParserContext parserContext = new TemplateParserContext();
/**
* 方法参数解析器
*/
private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
/**
* \@within(rateLimiter) 和 \@annotation(rateLimiter) 必须按照这个顺序,才会优先执行方法上的注解
*/
@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("服务器限流异常,请稍候再试", e);
}
}
}
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
// 判断 key 不为空 和 不是表达式
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) {
// 获取请求ip
str.append(ServletUtil.getClientIP(request)).append(":");
} else if (rateLimiter.limitType() == LimitType.CLUSTER) {
// 获取客户端实例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的配置跟我们注解上面的值不一致,说明服务器重启过,程序员又修改了限流的配置
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;
}
}
/**
* 获取客户端实例
*/
public static RedissonClient getClient() {
return CLIENT;
}
}
4.捕获异常
@Data
@EqualsAndHashCode(callSuper = true)
public class RateLimiterException extends RuntimeException {
/**
* 错误提示
*/
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("请求地址'{}', 限流异常'{}'", 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;
}
}
到这里,已经实现了一个完整的API接口限流功能。
可以将之进一步封装,作为一个springboot的starter,用于任意一个项目中。
小结
通过上述步骤,我们已经成功实现了一个基于Redisson和Spring AOP的API接口限流功能。这个方案不仅简单易懂,而且非常灵活,可以通过注解轻松地应用到任意方法上,并且支持多种限流策略(如全局限流、IP限流、集群限流等)。