接口访问频率限流
快速开始:四、代码实现 -> 6.配置RateLimit注解,使限流生效
一、限流场景
-
淘宝秒杀活动,限1小时200件商品
-
一个用户、一个手机号一天只能获取5次验证码
-
限制某个接口一分钟最多只能访问500次
二、处理方式
-
抛异常
-
排队等待
-
服务降级
三、令牌桶算法流程
四、代码实现
1.频率限制注解类
/**频率限制注解
* @author zhoujialin
* @time 2022/1/20 16:22
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/**
* 限制对象键
* @param
* @return java.lang.String
*/
String key() default "";
/**
* 一个周期(默认为1秒)生成令牌数
* @param
* @return long
*/
long rate();
/**
* 周期(目前支持SECONDS、MINUTES、HOURS、DAYS,默认SECONDS,如果指定了其他类型将抛出异常)
* @param
* @return java.util.concurrent.TimeUnit
*/
TimeUnit cycle() default TimeUnit.SECONDS;
/**
* 获取令牌数
* @param
* @return long
*/
long requested() default 1;
/**
* 提示信息
* @param
* @return java.lang.String
*/
String msg() default "";
}
2.频率限制配置类
/**
* 频率限制配置
* @author: zhoujialin
* @time: 2022/2/17 13:02
*/
public class RateLimiterConfig {
/**
* 限制对象键
*/
private String key;
/**
* 每周期生成令牌数
*/
private long rate;
/**
* 周期
*/
private TimeUnit cycle;
/**
* 申请令牌数
*/
private long requested;
/**
* 提示信息
*/
private String msg;
public static Builder builder(String key, long rate) {
return new Builder(key, rate);
}
private RateLimiterConfig(Builder builder) {
this.key = builder.key;
this.rate = builder.rate;
this.cycle = builder.cycle;
this.requested = builder.requested;
this.msg = builder.msg;
}
public String getKey() {
return key;
}
public long getRate() {
return rate;
}
public TimeUnit getCycle() {
return cycle;
}
public long getRequested() {
return requested;
}
public String getMsg() {
return msg;
}
public static class Builder {
/**
* 限制对象键
*/
private String key;
/**
* 每周期生成令牌数
*/
private long rate;
/**
* 周期
*/
private TimeUnit cycle;
/**
* 申请令牌数
*/
private long requested;
private String msg = "RateLimiter does not permit further calls";
private static final Set<TimeUnit> cycles = new HashSet<>();
static {
cycles.add(SECONDS);
cycles.add(MINUTES);
cycles.add(HOURS);
cycles.add(DAYS);
}
public Builder(String key, long rate) {
this.key = key;
this.rate = rate;
}
public Builder cycle(TimeUnit cycle) {
this.cycle = cycle;
return this;
}
public Builder requested(long requested) {
this.requested = requested;
return this;
}
public Builder msg(String msg) {
if(!Strings.isEmpty(msg)) {
this.msg = msg;
}
return this;
}
public RateLimiterConfig build() {
if(key == null) {
throw new RateLimitAcquireException("key cannot be null");
}
if(rate < 1) {
throw new RateLimitAcquireException("rate must be a positive");
}
if(requested < 1) {
throw new RateLimitAcquireException("requested must be a positive");
}
if(!cycles.contains(cycle)) {
throw new RateLimitAcquireException("the value of cycle must be SECONDS、MINUTES、HOURS or DAYS");
}
return new RateLimiterConfig(this);
}
}
}
3.频率限制接口
/**
* 频率限制接口
* @author zhoujialin
* @time 2022/2/17 11:09
*/
@FunctionalInterface
public interface RateLimiter {
/**
* 获得许可
* @param config 频率限制配置
* @return boolean
*/
boolean acquirePermission(RateLimiterConfig config);
/**
* 等待获得许可
* @param config 频率限制配置
* @return void
* @throws RateLimitAcquireException
*/
default void waitForPermission(RateLimiterConfig config) throws RateLimitAcquireException {
boolean permission = acquirePermission(config);
if (!permission) {
throw new RateLimitAcquireException(config.getMsg());
}
}
}
4.默认频率限制器
/**
* 默认频率限制器
*
* @author: zhoujialin
* @time: 2022/2/17 11:14
*/
public class DefaultRateLimiter implements RateLimiter {
/**
* 上次刷新令牌的时间(秒)
*/
private static Map<String, Long> lastRefreshedMap = new HashMap<>();
/**
* 上次剩余令牌数
*/
private static Map<String, Long> lastTokensMap = new HashMap<>();
/**
* 基于本地令牌桶的实现
* @param config 频率限制配置
* @return boolean
*/
@Override
public boolean acquirePermission(RateLimiterConfig config) {
String key = config.getKey();
long rate = config.getRate();
long capacity = rate;
TimeUnit cycle = config.getCycle();
long requested = config.getRequested();
//细节key是String类型,值相等时,不一定是同个对象,使用key.intern()是为了把值相等的key当同一个对象加锁
synchronized (key.intern()) {
//获取系统时间
long now = getNow(cycle);
//获取上次刷新令牌的时间(首次为0)
long lastRefreshed = lastRefreshedMap.getOrDefault(key, 0L);
//距上次刷新令牌的时间差
long delta = now - lastRefreshed;
//超时时间为令牌生成周期的2倍
if(lastRefreshed > 0 && delta < 2) {
//上次生成的令牌未过期,根据时间差生成的新的令牌数,重新计算桶中剩余令牌数
capacity = Math.min(capacity, lastTokensMap.getOrDefault(key, 0L) + delta * rate);
}
//桶中剩余令牌数大于将要获取令牌数时允许接口的访问,否则拒绝
boolean allowed = capacity >= requested;
//更新刷新令牌的时间
lastRefreshedMap.put(key, now);
if(!allowed) {
return false;
}
//更新桶中的令牌数
lastTokensMap.put(key, capacity - requested);
return true;
}
}
/**
* 按周期单位换算当前时间
* @param cycle
* @return long
*/
private long getNow(TimeUnit cycle) {
if(cycle == MINUTES) {
return (long) Math.floor(Instant.now().getEpochSecond() / 60);
}
if(cycle == HOURS) {
return (long) Math.floor(Instant.now().getEpochSecond() / 3600);
}
if(cycle == DAYS) {
return (long) Math.floor(Instant.now().getEpochSecond() / 86400);
}
return Instant.now().getEpochSecond();
}
}
5.请求频率限制切面
/**
* 请求频率限制切面
* @author zhoujialin
* @time 2022/2/16 16:54
*/
@Aspect
@Configuration
public class RateLimitAspect {
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint point, RateLimit rateLimit) throws Throwable {
String key = rateLimit.key();
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//key的前缀是"类名.方法"
String prefix = point.getTarget().getClass().getSimpleName() + "." + method.getName();
if(key.startsWith("#")) {
//如果key的值以"#"开头,表明值是spring的Expression表达式,需要用以下方法解析
//如key="#user.name",表示被代理方法的参数user的属性name的值
key = "." + ExpressionParserUtils.getValue(point, key);
}
//从注解中取出相关属性,用构造者模式初始化RateLimiterConfig,并调用获取令牌方法,如果调用失败,将抛出异常,成功则继续执行业务方法
rateLimiter().waitForPermission(RateLimiterConfig
.builder(prefix + key, rateLimit.rate())
.cycle(rateLimit.cycle())
.requested(rateLimit.requested())
.msg(rateLimit.msg())
.build());
return point.proceed();
}
@Bean
public RateLimiter rateLimiter() {
return new DefaultRateLimiter();
}
}
6.配置RateLimit注解,使限流生效
@RestController
@Api(tags = "测试")
@RequestMapping("/test")
public class TestController {
@GetMapping("rateLimit")
@ApiOperation("按接口访问频率限流(基于令牌桶算法)")
@RateLimit(key = "#user.username", rate = 10)
public boolean rateLimit(User user) {
//do something
//处理方式
//1.默认抛异常
//2.如果想以排队等待或服务降级的方式处理,可以将注解添加在下游的service上,这里捕获RateLimitAcquireException后,发mq或返回降级后的展示数据
return true;
}
}
五、测试
1.限流配置:每秒10个请求
2.操作方式:jmeter中每隔5ms发送1个请求,总共发送100个请求
3.请求时间段15:02:56.083~15:02:58.111
a. 56s期间总共32个请求,前10个成功,其他全部失败
b. 57s期间总共61个请求,前10个成功,其他全部失败