API接口限流竟然如此简单

开发 前端
我们已经成功实现了一个基于Redisson和Spring AOP的API接口限流功能。这个方案不仅简单易懂,而且非常灵活,可以通过注解轻松地应用到任意方法上,并且支持多种限流策略(如全局限流、IP限流、集群限流等)。

简介

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限流、集群限流等)。

责任编辑:武晓燕 来源: Java技术指北
相关推荐

2024-08-28 08:42:21

API接口限流

2020-12-28 07:47:35

动态代理AOP

2021-12-08 10:36:46

JavaPDF文件

2021-12-09 09:02:53

JavaPDF文件iText

2024-09-09 11:35:35

2022-08-12 12:19:13

Cluster检索集群

2020-06-19 17:49:23

建网

2018-08-27 08:31:25

InnoDBMySQL

2020-02-20 16:07:45

IT需求

2022-01-09 23:38:42

通信协议网络

2022-02-23 20:42:40

HTMLmarkdownturndown

2021-05-14 07:45:07

Sentinel 接口限流

2022-07-08 14:35:05

Java组件LiteFlow

2010-08-25 21:50:36

配置DHCP

2020-11-24 08:02:26

API接口重构

2023-08-21 08:01:03

2011-10-11 10:53:29

Ubuntu 11.1Gnome 3.2

2009-04-29 01:39:57

破解美萍万象

2021-03-30 10:46:42

SpringBoot计数器漏桶算法

2011-09-15 10:35:12

Android应用IOS应用着装搭配
点赞
收藏

51CTO技术栈公众号