使用Java自定义注解实现一个简单的令牌桶限流器

 更新时间:2023年10月08日 08:30:59   作者:酸奶小肥阳  
限流是在分布式系统中常用的一种策略,它可以有效地控制系统的访问流量,保证系统的稳定性和可靠性,在本文中,我将介绍如何使用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<String, BucketLog> 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<String, RateLimit>();
        for (int i = 0; i < annotations.length; i++) {
            RateLimit annotation = annotations[i];
            final String prefix = StringUtils.isBlank(annotation.prefixKey()) ?
                    method.getDeclaringClass() + "#" + method.getName() + ":index:" + i : annotation.prefixKey();
            String key = "";
            switch (annotation.target()) {
                case EL:
                    key = spELUtil.getArgValue(annotation.spEl(), joinPoint);
                    break;
                case USER:
                    key = rbacUserService.getCurrentUser();
            }
            var1.put(prefix + ":" + key, annotation);
        }
        var1.forEach((k, v) -> {
            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<ProjectVO> 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<Void> export(@PathVariable String projectId,
                               @RequestBody @Valid ExportDTO exportDTO) {
        projectService.export(projectId, exportDTO);
        return Result.ok();
    }
}

总结

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

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

到此这篇关于基于Java自定义注解实现一个令牌桶限流的文章就介绍到这了,更多相关Java实现令牌桶限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • spring boot中的properties参数配置详解

    spring boot中的properties参数配置详解

    这篇文章主要介绍了spring boot中的properties参数配置,需要的朋友可以参考下
    2017-09-09
  • Java的Character类详解

    Java的Character类详解

    在实际开发过程中,我们经常会遇到需要使用对象,而不是内置数据类型的情况。为了解决这个问题,Java语言为内置数据类型char提供了包装类Character类。本文详细介绍了Java的Character类,感兴趣的同学可以参考阅读
    2023-04-04
  • 这么设置IDEA中的Maven,再也不用担心依赖下载失败了

    这么设置IDEA中的Maven,再也不用担心依赖下载失败了

    今天给大家带来一个IDEA中Maven设置的小技巧.这个技巧可以说非常有用,学会设置之后,再也不用担心maven依赖下载变慢的问题,需要的朋友可以参考下
    2021-05-05
  • SpringMVC教程之json交互使用详解

    SpringMVC教程之json交互使用详解

    本篇文章主要介绍了SpringMVC教程之json使用详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • 如何在Java中优雅地使用正则表达式详解

    如何在Java中优雅地使用正则表达式详解

    这篇文章主要给大家介绍了关于如何在Java中优雅地使用正则表达式的相关资料,正则表达式就是一个字符串,但和普通的字符串不同的是,正则表达式是对一组相似字符串的抽象,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-02-02
  • 详解Spring Security 中的四种权限控制方式

    详解Spring Security 中的四种权限控制方式

    这篇文章主要介绍了详解Spring Security 中的四种权限控制方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • spring boot集成WebSocket日志实时输出到web页面

    spring boot集成WebSocket日志实时输出到web页面

    这篇文章主要为大家介绍了spring boot集成WebSocket日志实时输出到web页面展示的详细操作,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-03-03
  • 最长重复子数组 findLength示例详解

    最长重复子数组 findLength示例详解

    今天给大家分享一道比较常问的算法面试题,最长重复子数组 findLength,文中给大家分享解题思路,结合示例代码介绍的非常详细,需要的朋友参考下吧
    2023-08-08
  • java正则表达式匹配规则超详细总结

    java正则表达式匹配规则超详细总结

    正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别,下面这篇文章主要给大家介绍了关于java正则表达式匹配规则的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-10-10
  • Java中Servlet的生命周期

    Java中Servlet的生命周期

    这篇文章主要介绍了Java中Servlet的生命周期,Servlet 初始化后调用 init () 方法、Servlet 调用 service() 方法来处理客户端的请求、Servlet 销毁前调用 destroy() 方法,下面来看看具体的解析吧,需要的小伙伴可以参考一下
    2022-01-01

最新评论