Skip to content

引言

限流(Rate Limiting)是一种保护系统免受过多请求冲击的技术手段。通过限制单位时间内的请求数量,可以防止系统过载、避免资源耗尽,并确保服务的稳定性。

项目中集成限流功能的主要场景包括:

防止恶意请求:限制单个用户或 IP 的请求频率。
保护核心接口:确保核心业务接口不会被过多请求拖垮。
流量控制:在高峰期平滑流量,避免系统崩溃。

AOP优势

在项目中使用 AOP(面向切面编程)注解实现防重复提交具有以下优势:

1. 代码解耦:将日志记录逻辑与业务逻辑分离,避免代码重复,提升代码可读性和可维护性。
2. 灵活性与扩展性:通过注解可以灵活地控制哪些方法需要记录日志,便于扩展和修改日志记录逻辑。
3. 非侵入性:无需修改现有业务代码,只需在方法上添加注解即可实现日志记录。
4. 集中管理:日志记录逻辑集中在切面中,便于统一管理和维护。
5. 提高开发效率:通过注解方式快速实现日志功能,减少重复代码编写。

定义注解

xiaomayi-common/xiaomayi-ratelimiter 模块中定义的防重复提交的 AOP 切面 RateLimiter 文件,内容如下:

js
package com.xiaomayi.ratelimiter.annotation;

import com.xiaomayi.ratelimiter.enums.LimiterType;

import java.lang.annotation.*;

/**
 * <p>
 * 限流注解
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2023-06-16
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RateLimiter {

    /**
     * 限流KEY,支持Spring的el表达式动态获取方法参数
     * 使用案例:#code.id #{#code}
     *
     * @return 返回结果
     */
    String key() default "";

    /**
     * 限流时间,单位:秒
     *
     * @return 返回结果
     */
    int time() default 60;

    /**
     * 限流次数
     *
     * @return 返回结果
     */
    int count() default 100;

    /**
     * 限流类型
     *
     * @return 返回结果
     */
    LimiterType type() default LimiterType.DEFAULT;

    /**
     * 限流提示语,支持国际化,格式案例:{code}
     *
     * @return 返回结果
     */
    String message() default "{rate.limiter.message}";

}

注解实现

js
package com.xiaomayi.ratelimiter.aspect;

import cn.hutool.extra.spring.SpringUtil;
import com.xiaomayi.core.constant.CacheConstant;
import com.xiaomayi.core.exception.BizException;
import com.xiaomayi.core.utils.MessageUtils;
import com.xiaomayi.core.utils.ServletUtils;
import com.xiaomayi.core.utils.StringUtils;
import com.xiaomayi.ratelimiter.annotation.RateLimiter;
import com.xiaomayi.ratelimiter.enums.LimiterType;
import com.xiaomayi.redis.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RateType;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.lang.reflect.Method;

/**
 * <p>
 * 限流AOP切面
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2023-06-16
 */
@Slf4j
@Aspect
public class RateLimiterAspect {

    /**
     * 定义表达式解析器
     */
    private final ExpressionParser parser = new SpelExpressionParser();

    /**
     * 定义解析模版
     */
    private final ParserContext parserContext = new TemplateParserContext();

    /**
     * 方法参数解析器
     */
    private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();

    /**
     * 切点之前调用
     *
     * @param point       切点
     * @param rateLimiter 限流注解
     */
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        try {
            String combineKey = getCombineKey(rateLimiter, point);
            RateType rateType = RateType.OVERALL;
            if (rateLimiter.type() == LimiterType.CLUSTER) {
                rateType = RateType.PER_CLIENT;
            }
            long number = RedisUtils.rateLimiter(combineKey, rateType, count, time);
            if (number == -1) {
                String message = rateLimiter.message();
                if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
                    message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
                }
                throw new BizException(message);
            }
            log.info("限制令牌 => {}, 剩余令牌 => {}, 缓存key => '{}'", count, number, combineKey);
        } catch (Exception e) {
            if (e instanceof BizException) {
                throw e;
            } else {
                throw new RuntimeException("服务器限流异常,请稍候再试", e);
            }
        }
    }

    /**
     * 获取组合KEY
     *
     * @param rateLimiter 限流注解
     * @param point       AOP切点
     * @return 返回结果
     */
    private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        String key = rateLimiter.key();
        if (StringUtils.isNotBlank(key)) {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method targetMethod = signature.getMethod();
            Object[] args = point.getArgs();
            //noinspection DataFlowIssue
            MethodBasedEvaluationContext context =
                    new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
            context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getBeanFactory()));
            Expression expression;
            if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
                    && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
                expression = parser.parseExpression(key, parserContext);
            } else {
                expression = parser.parseExpression(key);
            }
            key = expression.getValue(context, String.class);
        }
        StringBuilder stringBuffer = new StringBuilder(CacheConstant.RATE_LIMIT_KEY);
        stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":");
        if (rateLimiter.type() == LimiterType.IP) {
            // 获取客户端请求IP
            stringBuffer.append(ServletUtils.getClientIP()).append(":");
        } else if (rateLimiter.type() == LimiterType.CLUSTER) {
            // 获取客户端实例id
            stringBuffer.append(RedisUtils.getClient().getId()).append(":");
        }
        return stringBuffer.append(key).toString();
    }

}

添加依赖

pom.xml 配置文件中引入以下依赖:

js
<!-- 限流模块 -->
<dependency>
    <groupId>com.xiaomayi</groupId>
    <artifactId>xiaomayi-ratelimiter</artifactId>
</dependency>

注解使用

在需要设置限流的方法上添加以下注解:

js
@RateLimiter(count = 2, time = 10)

使用案例:

js
package com.xiaomayi.admin.controller.demo;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xiaomayi.admin.entity.DataModel;
import com.xiaomayi.core.utils.R;
import com.xiaomayi.ratelimiter.annotation.RateLimiter;
import com.xiaomayi.system.entity.DictItem;
import com.xiaomayi.system.entity.User;
import com.xiaomayi.system.service.UserService;
import com.xiaomayi.system.utils.DictResolver;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

/**
 * <p>
 * 测试案例 前端控制器
 * 特别备注:此文件非项目本身有效文件,仅仅是工程师编写测试案例使用,留个备份未删除
 * 实际项目使用时请删除此文件,以免造成其他影响
 * </p>
 *
 * @author 小蚂蚁云团队
 * @since 2024-05-21
 */
@Slf4j
@RestController
@RequestMapping("/demo")
@AllArgsConstructor
public class DemoController {

    private final UserService userService;

    /**
     * 全局限流
     *
     * @param value 参数值
     * @return 返回结果
     */
    @RateLimiter(count = 2, time = 10)
    @GetMapping("/ratelimiter")
    public R test(String value) {
        return R.ok("操作成功", value);
    }

}

限流验证

  1. 正常请求

请求网络请求时会自动放行并返回结果:

js
{
    "code": 0,
    "msg": "hello",
    "data": "操作成功",
    "ok": true
}

使用 ApiFox 调试工具测试结果如下图:

  1. 触发限流机制

网络请求频次超过设置的限流注解参数时,会自动触发限流机制,并返回限流响应结果:

js
{
    "code": 1,
    "msg": "访问过于频繁,请稍候再试",
    "data": null,
    "ok": false
}

使用 ApiFox 调试工具测试结果如下图:

总结

根据项目需求选择合适的限流方案,可以有效保护系统免受过多请求的冲击,提升系统的稳定性和可靠性。

小蚂蚁云团队 · 提供技术支持

小蚂蚁云 新品首发
新品首发,限时特惠,抢购从速! 全场95折
赋能开发者,助理企业发展,提供全方位数据中台解决方案。
获取官方授权