基于Java自定义注解实现一个令牌桶限流

2023年 9月 27日 35.6k 0

限流是在分布式系统中常用的一种策略,它可以有效地控制系统的访问流量,保证系统的稳定性和可靠性。在本文中,我将介绍如何使用Java自定义注解来实现一个简单的令牌桶限流器。

什么是令牌桶限流?

令牌桶限流是一种常用的限流算法,它基于令牌桶的概念。在令牌桶中,令牌以固定的速率被生成并放置其中。当一个请求到达时,它必须获取一个令牌才能继续执行,否则将被阻塞或丢弃。

开始我们的实现

第一步:创建一个自定义注解

我们首先需要创建一个自定义注解,用于标识需要进行限流的方法。这个注解可以命名为@RateLimit,它可以带有以下几个参数

  • rate: 表示该方法的限流速率,单位可以是每秒请求数(QPS)。
  • prefixKey: 针对不同方法上对同一个资源做限流的情况。
  • target: 限流的对象,默认使用spEl表达式对入参进行获取
  • capacity: 令牌桶容量,满了之后令牌不再增加

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {

    /**
     * key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做频控,就自己指定
     */
    String prefixKey() default "";

    /**
     * 选择目标类型: 1.EL,需要在spEl()中指定限流资源。2.USER,针对用户进行限流
     */
    Target target() default Target.EL;

    /**
     * springEl表达式 指定频控对象
     */
    String spEl() default "";

    /**
     * 令牌桶容量
     */
    double capacity() default 10;

    /**
     * 令牌生成速率 n/秒
     */
    double rate() default 1;

    enum Target {
        EL, USER
    }

}

第二步:实现限流逻辑

接下来,我们需要编写一个类来处理限流逻辑。这个类可以命名为RateLimitAspect,它将会扫描所有被@RateLimit注解标记的方法,并在必要时进行限流。

/**
 * 令牌桶限流
 *
 * @date 2023/07/07
 */
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class RateLimitAspect {


    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private RbacUserService rbacUserService;

    private final SpELUtil spELUtil;

    @Around("@annotation(com.netease.fuxi.config.annotation.RateLimit)")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        RateLimit[] annotations = method.getAnnotationsByType(RateLimit.class);
        var var1 = new HashMap();
        for (int i = 0; i  {
            var var2 = Boolean.TRUE.equals(redisTemplate.hasKey(k)) ? redisTemplate.opsForValue().get(k) :
                    new BucketLog(v.capacity(), Instant.now().getEpochSecond());
            long nowTime = Instant.now().getEpochSecond();
            double addTokens = (nowTime - var2.getLastRefillTime()) * v.rate();
            // 如果生成的令牌超过的桶最大容量,那么令牌数取桶最大容量
            var2.setTokens(Math.min(var2.getTokens() + addTokens, v.capacity()));
            var2.setLastRefillTime(nowTime);
            double remain = var2.getTokens() - 1;
            if (remain < 0) {
                throw new BusinessException("操作太频繁,请稍后重试", 42901);
            }
            var2.setTokens(remain);
            long timeout = (long) Math.ceil(v.capacity() / v.rate());// redis过期时间设置大于 容量/速率
            redisTemplate.opsForValue().set(k, var2, timeout, TimeUnit.SECONDS);
        });

        return joinPoint.proceed();

    }
}

SpEL解析工具类

@Component
public class SpELUtil {

    /**
     * 获取表达式中的参数值
     *
     * @param expr      表达式
     * @param joinPoint 切点
     * @return 参数值
     */
    public String getArgValue(String expr, JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = getParameterNames(joinPoint);
        EvaluationContext context = new StandardEvaluationContext();
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        ExpressionParser parser = new SpelExpressionParser();
        return parser.parseExpression(expr).getValue(context, String.class);
    }

    /**
     * 获取方法的参数名称
     *
     * @param joinPoint 切点
     * @return 参数名称
     */
    private String[] getParameterNames(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] parameterNames = signature.getParameterNames();
        if (parameterNames == null || parameterNames.length == 0) {
            ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
            parameterNames = parameterNameDiscoverer.getParameterNames(signature.getMethod());
        }
        return parameterNames;
    }
}

第三步:在方法上使用@RateLimit注解

现在,我们可以在需要进行限流的方法上使用@RateLimited注解,指定相应的限流速率。

示例1: 限制了令牌桶容量10,每10秒生成一个令牌,限制对象为当前用户。

@Api(tags = "项目服务")
@Validated
@Slf4j
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class ProjectController {

    @Resource
    private IProjectService projectService;

    @ApiOperation("创建项目")
    @PostMapping("/project")
    @RateLimit(capacity = 10, rate = 0.1, target = RateLimit.Target.USER)
    public Result createProject() {
        ProjectVO projectVO = projectService.createProject();
        return Result.ok(projectVO);
    }
}

示例2: 限制了令牌桶容量1,每2秒生成一个令牌,限制对象为该项目。


@Slf4j
@RestController
@RequestMapping("/api/v1")
public class ProjectController {

    @Resource
    private IProjectService projectService;

    @ApiOperation("数据导出")
    @PostMapping("/project/{projectId}/export")
    @RateLimit(capacity = 1, rate = 0.5, spEl = "#projectId")
    public Result export(@PathVariable String projectId,
                               @RequestBody @Valid ExportDTO exportDTO) {

        projectService.export(projectId, exportDTO);
        return Result.ok();
    }
}

总结

通过使用Java自定义注解,我们成功地实现了一个简单的令牌桶限流器。这个限流器可以方便地应用于需要对访问速率进行控制的方法中,保证系统的稳定性和可靠性。

在实际项目中,我们可以根据需求对限流器进行进一步地扩展和优化,以满足不同场景下的限流需求。希望本文对你理解和实现限流算法有所帮助!

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论