应用场景
API接口的流量控制策略:缓存、降级、限流。限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。限流策略虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用。
常用的限流策略:
Nginx 限流
按照一定的规则如帐号、IP、系统间调用逻辑等在 Nginx 层面做限流
业务系统限流
客户端限流
服务端限流
数据库限流
常用限流算法
计数器
计数器是最简单粗暴的算法,通过直接统计每个时间的请求数目来判断是否需要拒绝。
比如某个服务最多只能每秒钟处理 100 个请求。我们可以设置一个 1 秒钟的滑动窗口,窗口中有 10 个格子,每个格子 100 毫秒,每 100 毫秒移动一次,每次移动都需要记录当前服务请求的次数。内存中需要保存 10 次的次数。可以用数据结构 LinkedList 来实现。格子每次移动的时候判断一次,当前访问次数和 LinkedList 中最后一个相差是否超过 100 ,如果超过就需要限流了。
很明显,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
计数器的实现简单,但是是平均分配1秒钟的请求,然而实际情况中的请求往往是动态的,流量不平滑的。
漏桶
漏桶( Leaky Bucket )算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。
可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水( burst ),另一个是水桶漏洞的大小( rate )。
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发( burst )到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.
令牌桶
令牌桶算法( Token Bucket )和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100 ,则间隔是 10ms )往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个 Token ,如果没有 Token 可拿了就阻塞或者拒绝服务.
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.
基于Redis的令牌桶算法限流策略实现
策略
因为出现了某些客户突然加大流量的情况,为了避免抢占其他用户的资源,所以设计了基于用户(限制单个用户的最大请求数)的令牌桶策略。
JAVA 实现
/**
* 获取令牌
*
* @param key 令牌类别标识(每个用户不同)
* @param permits 请求的令牌数量
* @param currMillSecond 当前毫秒数
* @return 是否能请求到令牌
*/
public boolean acquire(String key, Integer permits, long currMillSecond) {
try (Jedis jedis = JedisPoolUtil.getJedisPool().getResource()) {
//针对新用户创建令牌桶
if (!jedis.exists(key)) {
jedis.hset(key, "last_mill_second", String.valueOf(currMillSecond));
jedis.hset(key, "curr_permits", "0");
jedis.hset(key, "max_permits", "500");
jedis.hset(key, "rate", "400");
return true;
}
//获取令牌桶信息,上一个令牌时间,当前可用令牌数,最大令牌数,令牌消耗速率
List<String> limitInfo = jedis.hmget(key, "last_mill_second", "curr_permits", "max_permits", "rate");
long lastMillSecond = Long.parseLong(limitInfo.get(0));
Integer currPermits = Integer.valueOf(limitInfo.get(1));
Integer maxPermits = Integer.valueOf(limitInfo.get(2));
Double rate = Double.valueOf(limitInfo.get(3));
//向桶里面添加令牌
Double reversePermitsDouble = ((currMillSecond - lastMillSecond) / 1000) * rate;
Integer reversePermits = reversePermitsDouble.intValue();
Integer expectCurrPermits = reversePermits + currPermits;
Integer localCurrPermits = Math.min(expectCurrPermits, maxPermits);
//添加令牌之后更新时间
if (reversePermits > 0) {
jedis.hset(key, "last_mill_second", String.valueOf(currMillSecond));
}
//判断桶里面剩余的令牌数目
if (localCurrPermits - permits >= 0) {
jedis.hset(key, "curr_permits", String.valueOf(localCurrPermits - permits));
return true;
} else {
jedis.hset(key, "curr_permits", String.valueOf(localCurrPermits));
return false;
}
} catch (Exception e) {
return false;
}
}
使用方式:
if (!rateLimiterService.acquire("limiter:" + uid, 1, System.currentTimeMillis())) {
throw new BusinessException(ExceptionType.TOO_BUSY);
}