互联网服务赖以生存的根本是流量, 产品和运营会经常通过各种方式来为应用倒流,比如淘宝的双十一等,如何让系统在处理高并发的同时还是保证自身系统的稳定,通常在最短时间内提高并发的做法就是加机器,但是如果机器不够怎么办?那就需要做业务降级或系统限流。
流量控制中用的比较多的三个算法就是令牌桶、漏桶、计数器。
1.令牌桶限流(TokenBucket)
令牌桶算法的基本过程如下:
- 每秒会有 r 个令牌放入桶中,或者说,每过 1/r 秒桶中增加一个令牌。
- 桶中最多存放 b 个令牌,如果桶满了,新放入的令牌会被丢弃。
- 当一个 n 字节的数据包到达时,消耗 n 个令牌,然后发送该数据包。
- 如果桶中可用令牌小于 n,则该数据包将被缓存或丢弃。
2.漏桶限流(LeakBucket)
漏桶算法强制一个常量的输出速率而不管输入数据流的突发性,当输入空闲时,该算法不执行任何动作.就像用一个底部开了个洞的漏桶接水一样,水进入到漏桶里,桶里的水通过下面的孔以固定的速率流出,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率.如下图所示:
漏桶和令牌桶比较
“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输数据外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的上限,因此它适合于具有突发特性的流量。
计数器限流
有时我们还会使用计数器来进行限流,主要用来限制一定时间内的总并发数。计数器限流方法可以通过缓存实现计数器,假如以秒为单位进行限流,过期时间为1秒,每次请求计数加1,超过每秒允许的最大请求数请求数阀值将被丢弃。
RateLimiter
我们可以使用 Guava 的 RateLimiter 来实现基于令牌桶的流量控制。RateLimiter 令牌桶算法的单桶实现,RateLimiter 对简单的令牌桶算法做了一些工程上的优化,具体的实现是 SmoothBursty。需要注意的是,RateLimiter 的另一个实现 SmoothWarmingUp,就不是令牌桶了,而是漏桶算法。
SmoothBursty 有一个可以放 N 个时间窗口产生的令牌的桶,系统空闲的时候令牌就一直攒着,最好情况下可以扛 N 倍于限流值的高峰而不影响后续请求,就像三峡大坝一样能扛千年一遇的洪水.
4.redis
local key = KEYS[1] --限流KEY(一秒一个) local limit = tonumber(ARGV[1]) --限流大小 local current = tonumber(redis.call(‘get‘, key) or "0") if current + 1 > limit then --如果超出限流大小 redis.call("INCRBY", key,"1") -- 如果不需要统计真是访问量可以不加这行 return 0 else --请求数+1,并设置2秒过期 redis.call("INCRBY", key,"1") if tonumber(ARGV[2]) > -1 then redis.call("expire", key,tonumber(ARGV[2])) --时间窗口最大时间后销毁键 end return 1 end
lua脚本返回值比较奇怪,用java客户端接受返回值,只能使用Long,没有去深究。这个脚本只需要传入key(url+时间戳/预设时间窗口大小),便可以实现限流。
java调用lua脚本
/** * Created by xujingfeng on 2017/3/13. * <p> * 基于redis lua脚本的线程安全的计数器限流方案 * </p> */ public class RedisRateLimiter { /** * 限流访问的url */ private String url; /** * 单位时间的大小,最大值为 Long.MAX_VALUE - 1,以秒为单位 */ final Long timeUnit; /** * 单位时间窗口内允许的访问次数 */ final Integer limit; /** * 需要传入一个lua script,莫名其妙redisTemplate返回值永远是个Long */ private RedisScript<Long> redisScript; private RedisTemplate redisTemplate; /** * 配置键是否会过期, * true:可以用来做接口流量统计,用定时器去删除 * false:过期自动删除,时间窗口过小的话会导致键过多 */ private boolean isDurable = false; public void setRedisScript(RedisScript<Long> redisScript) { this.redisScript = redisScript; } public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public boolean isDurable() { return isDurable; } public void setDurable(boolean durable) { isDurable = durable; } public RedisRateLimiter(Integer limit, Long timeUnit) { this.timeUnit = timeUnit; Assert.isTrue(timeUnit < Long.MAX_VALUE - 1); this.limit = limit; } public RedisRateLimiter(Integer limit, Long timeUnit, boolean isDurable) { this(limit, timeUnit); this.isDurable = isDurable; } public boolean acquire() { return this.acquire(this.url); } public boolean acquire(String url) { StringBuffer key = new StringBuffer(); key.append("rateLimiter").append(":") .append(url).append(":") .append(System.currentTimeMillis() / 1000 / timeUnit); Integer expire = limit + 1; String convertExpire = isDurable ? "-1" : expire.toString(); return redisTemplate.execute(redisScript, Arrays.asList(key.toString()), limit.toString(), convertExpire).equals(1l); } }
5.spring mvc
作者:Lewe
链接:http://www.jianshu.com/p/7170edcd9239
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。