频控限流设计---云信方案

方案背景

频控限流是一种非常重要的技术手段,它主要用于控制对服务器或服务的访问频率,以防止服务器因请求过多而崩溃,同时也能够保护系统不会因为过载而影响服务质量。频控限流通常用于API接口调用网站访问服务端请求处理等多种场景。

频控&限流方案

频控限流的常见实现方式包括固定窗口算法滑动窗口算法漏桶算法令牌桶算法。固定窗口算法通过在固定时间窗口内计数请求次数来实现限流,但可能会在窗口切换时出现流量高峰。滑动窗口算法通过将时间窗口划分为多个小段,对每个小段内的请求进行计数,从而更细致地控制流量。漏桶算法通过以固定速率流出请求,而请求以任意速率流入,超出桶容量的请求将被丢弃,从而平滑流量。令牌桶算法则是在固定容量的桶中以固定速率添加令牌,请求需要消耗令牌才能被处理,允许一定程度的突发流量。

频控调用代码

	// 服务启动时初始化该限流器
    public boolean checkClientRequestFreq(String deviceId) {
        return this.camelliaFreq.checkFreqPass(generateKey(deviceId), freqType, freqConfig).isPass();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        logger.info("频控限流器初始化开始...");
        // 客户端需要做业务频控,防止异常流量,本地缓存即可
        this.camelliaFreq = new CamelliaFreq();
        this.freqType = CamelliaFreqType.STANDALONE;
        this.freqConfig = new CamelliaFreqConfig();
        this.freqConfig.setCheckTime(1000);
        this.freqConfig.setThreshold(5);
        this.freqConfig.setBanTime(10000);
        this.freqConfig.setDelayBanEnable(true);
        logger.info("频控限流器初始化结束.");
    }

频控参数

this.freqType = CamelliaFreqType.STANDALONE; 标识单机维度频控
this.freqConfig.setCheckTime(1000); 标识窗口大小
this.freqConfig.setThreshold(5); 标识该窗口最大请求次数
this.freqConfig.setBanTime(10000); 标识顺延的过期时间
this.freqConfig.setDelayBanEnable(true); 标识 是否启用 过期时间顺延

单机频控

  1. 单机频控是用ConcurrentLinkedHashMap储存每一个key对应的次数,即 如下:
	private final ConcurrentLinkedHashMap<String, Counter> cache;
	# 其中 key 标识需要进行频控的对象
	# Counter 是一个计数器,标识当前对象在该区间请求的次数
	# 且ConcurrentLinkedHashMap 默认根据LRU算法进行淘汰缓存中数据
    public CamelliaStandaloneFreq(int capacity) {
     	cache = new ConcurrentLinkedHashMap.Builder<String, Counter>()
                .initialCapacity(capacity).maximumWeightedCapacity(capacity).build();
   	}

	// map的初始化size
    public CamelliaStandaloneFreq() {
        this(100000);
    }

    private static class Counter {
        private final AtomicLong count = new AtomicLong();
        private long expireTime;

        public boolean isExpire() {
            return System.currentTimeMillis() > expireTime;
        }

        public long addAndGet(int delta) {
            return count.addAndGet(delta);
        }
    }
  1. 频控检测:根据代码可以看出,单机频控使用的是 滑动窗口 的方案,即每次选取固定时间片段的请求去处理。
	public CamelliaFreqResponse checkFreqPass(String freqKey, int delta, CamelliaFreqConfig freqConfig) {
        try {
            Counter counter = cache.get(freqKey);
            if (counter != null && counter.isExpire()) {
                cache.remove(freqKey);
                counter = null;
            }
            if (counter == null) {
                counter = new Counter();
                Counter oldCounter = cache.putIfAbsent(freqKey, counter);
                if (oldCounter != null) {
                    counter = oldCounter;
                }
            }
            long current = counter.addAndGet(delta);
            if (current == delta) {
                counter.expireTime = System.currentTimeMillis() + freqConfig.getCheckTime();
            }
            boolean pass = current <= freqConfig.getThreshold();
            if (!pass) {
                if (freqConfig.getBanTime() > 0) {
                    if (freqConfig.isDelayBanEnable()) {
                        counter.expireTime = System.currentTimeMillis() + freqConfig.getBanTime();
                    } else {
                        if (current <= freqConfig.getThreshold() + delta) {
                            counter.expireTime = System.currentTimeMillis() + freqConfig.getBanTime();
                        }
                    }
                }
            }
            return new CamelliaFreqResponse(pass, current, CamelliaFreqType.STANDALONE);
        } catch (Throwable e) {
            logger.error("checkFreqPass error, return pass, freqKey = {}, delta = {}, freqConfig = {}", freqKey, delta, JSONObject.toJSONString(freqConfig), e);
            return CamelliaFreqResponse.DEFAULT_PASS;
        }
    }

集群频控

思路:利用redis来保存集群维度下,当前key的访问次数,实际上也是滑动窗口的方法。
即:如果key不存在的话,新增一个key,并设置过期时间
反之,则给当前key对应的value值执行+1操作。
实现原子操作是借助 lua 脚本。

    public CamelliaFreqResponse checkFreqPass(String freqKey, int delta, CamelliaFreqConfig freqConfig) {
        try {
            Object curObj = template.eval("local x = redis.call('incrBy', KEYS[1], ARGV[1])\n" +
                    "if x == tonumber(ARGV[1]) then\n" +
                    "\tredis.call('pexpire', KEYS[1], ARGV[2])\n" +
                    "end\n" +
                    "return x", 1, freqKey, String.valueOf(delta), String.valueOf(freqConfig.getCheckTime()));
            long current = Long.parseLong(String.valueOf(curObj));
            boolean pass = current <= freqConfig.getThreshold();
            if (!pass) {
                if (freqConfig.getBanTime() > 0) {
                    if (freqConfig.isDelayBanEnable()) {//如果惩罚时间顺延,则再惩罚时间范围内,每多一次请求,ban都会被顺延
                        template.pexpire(freqKey, freqConfig.getBanTime());
                    } else {
                        if (current <= freqConfig.getThreshold() + delta) {//不顺延,则只有到达阈值的那一次设置ban时间
                            template.pexpire(freqKey, freqConfig.getBanTime());
                        }
                    }
                }
            }
            return new CamelliaFreqResponse(pass, current, CamelliaFreqType.CLUSTER);
        } catch (Throwable e) {
            logger.error("checkFreqPass error, freqKey = {}, delta = {}, freqConfig = {}", freqKey, delta, JSONObject.toJSONString(freqConfig), e);
        }
        return CamelliaFreqResponse.DEFAULT_PASS;
    }

达到频控时解决方案

  1. 直接丢弃当前请求,并进行预警。
  2. 丢进消息队列慢慢消费

你可能感兴趣的:(java,开发语言)