在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 官方文档