使用Google工具类Guava自定义一个@Limiter接口限流注解

在Springboot中引用RateLimiter工具类依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version>
</dependency>

需要注意的是,Guava 的不同版本可能会有一些差异,因此建议根据自己的实际情况选择合适的版本。另外,如果你使用 Gradle 或其他构建工具来管理项目依赖,也可以根据上述 Maven 依赖进行相应的配置。

自定义一个@Limiter注解

import java.lang.annotation.*;

/**
 * @author admin
 * @Description 接口限流
 *
 * 注解 @Inherited 和 @Documented 的区别:
 * -> @Inherited允许其他注解继承该注解。
 * -> @Documented 可以被例如 javadoc此类的工具文档化,Documented是一个标注注解,没有成员。
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Limiter {

    // 接口名称
    String name() default "";

    // 接口限流速率,默认20
    int value() default 20;

}

定义切面类 LimiterAspect 实现@Limiter注解

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.RateLimiter;
import com.hrbb.common.core.utils.StringUtils;
import com.hrbb.risk.config.ConstantConfig;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @Author admin
 * @Description 接口限流注解
 **/
@Aspect
@Component
public class LimiterAspect {

    private static final String STR_SPLIT_ = "_";
    private static final Logger log = LoggerFactory.getLogger(LimiterAspect.class);

    /**
     * 通过get()方法获取限流实例
     * 如果实例不存在,则默认创建一个限流实例
     * 参数可以根据需求灵活定义
     */
    private static LoadingCache<String, RateLimiter> cacheMap = CacheBuilder.newBuilder()
            .expireAfterWrite(ConstantConfig.NUMBER_10, TimeUnit.SECONDS)
            .build(new CacheLoader<String, RateLimiter>() {
                @Override
                public RateLimiter load(String key) throws Exception {
                    String[] split = key.split(STR_SPLIT_);
                    log.info("系统限流:[{}]方法创建限流实例, 实例过期时间[10]秒,限流速率每秒[{}]QPS/s", split[0], split[1]);
                    cacheMap.put(key, RateLimiter.create(Double.valueOf(split[1])));
                    return cacheMap.get(key);
                }
            });


    @Before(value = "@annotation(limiter)")
    public void doBefore(JoinPoint joinPoint, Limiter limiter){

        // 如果没有设置接口限流名称、则默认取方法名称为key
        String limitName = limiter.name();
        if (StringUtils.isBlank(limiter.name())){
            limitName = joinPoint.getSignature().getName();
        }

        // 使用接口名称和过期时间,拼接keyName,然后在load中获取相关的参数
        String keyName = limitName + STR_SPLIT_ + limiter.value();
        try {
            // 创建限流实例
            RateLimiter rateLimiter = cacheMap.get(keyName);
            if (!rateLimiter.tryAcquire()) {
                log.error("接口请求失败,当前接口名称{},每秒请求速率超过{} QPS/s", limitName, limiter.value());
                //throw new RuntimeException("Rate limit exceeded");
            }
        }catch (Exception e){
            throw new RuntimeException(e.getMessage(),e);
        }

    }

}

验证注解

创建 TestLimiterController 类,查看注解在接口中的使用情况

import com.hrbb.common.core.web.domain.AjaxResult;
import com.hrbb.risk.annotation.Limiter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

/**
 * @Author admin
 * @Description 接口限流测试
 **/
@RestController
@RequestMapping("/test")
public class TestLimiterController {

    private static final Logger logger = LoggerFactory.getLogger(TestLimiterController.class);

    @Limiter(name = "limiterDemo", value = 30)
    @GetMapping("/limiterDemo")
    public AjaxResult limiterDemo()
    {
        logger.info("接口响应成功!");
        return AjaxResult.success("响应成功");
    }
}

使用 jmeter 压测,50个线程、30秒

控制台打印信息

 

关于 CacheBuilder.newBuilder().expireAfterWrite 

CacheBuilder.newBuilder().expireAfterWrite 方法本身是线程安全的,可以在多线程环境下使用。它返回的是一个 CacheBuilder 对象,该对象是不可变的,因此可以被多个线程共享。

然而,如果你使用 CacheBuilder 创建了一个缓存对象,并且在多个线程中同时访问该缓存对象,那么就需要注意线程安全问题了。具体来说,如果多个线程同时对同一个缓存对象进行读写操作,可能会导致数据不一致或者并发异常等问题。

为了避免这种情况,可以考虑使用 CacheLoader 或者 LoadingCache 来创建缓存对象,这样可以确保缓存对象的加载和更新是线程安全的。例如:

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(new CacheLoader<String, String>() {
            @Override
            public String load(String key) throws Exception {
                // 从数据库或其他数据源中加载数据
                return loadDataFromDatabase(key);
            }
        });

在上面的示例中,我们使用 CacheLoader 来创建缓存对象,并在其中实现了数据加载的逻辑。由于 CacheLoader 是线程安全的,因此可以确保缓存对象的加载和更新是线程安全的。

@Aspect 注解

@Aspect 是 Spring AOP 中的一个注解,用于声明一个切面类。切面类是一个普通的 Java 类,其中包含了一些切点和通知等组件,用于对目标对象进行增强。

具体来说,@Aspect 注解可以用在类上,表示该类是一个切面类。在切面类中,可以使用其他注解来定义切点和通知等组件,例如:

  • @Pointcut:定义一个切点,用于匹配目标对象中的方法。
  • @Before:定义一个前置通知,在目标方法执行之前执行。
  • @After:定义一个后置通知,在目标方法执行之后执行。
  • @Around:定义一个环绕通知,在目标方法执行前后都可以执行自定义逻辑。
  • @AfterReturning:定义一个返回通知,在目标方法返回结果之后执行。
  • @AfterThrowing:定义一个异常通知,在目标方法抛出异常时执行。

此外,还有一些其他的注解可以用于定义切面组件,具体可以参考 Spring AOP 的文档

Spring AOP 官方文档

猜你喜欢

转载自blog.csdn.net/amosjob/article/details/131234942