频控限流是一种非常重要的技术手段,它主要用于控制对服务器或服务的访问频率,以防止服务器因请求过多而崩溃,同时也能够保护系统不会因为过载而影响服务质量。频控限流通常用于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); 标识 是否启用 过期时间顺延
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);
}
}
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;
}