接口访问频率限流

快速开始:四、代码实现 -> 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个成功,其他全部失败


c. 58s期间总共7个请求,全部成功

4.聚合报告,异常73%,表明只有27个请求通过了,达到限流的目的

posted @ 2022-04-08 14:20  花弄影  阅读(525)  评论(0编辑  收藏  举报