赞
踩
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
常用的限流算法有令牌桶和,漏桶,滑动窗口
漏桶(Leaky Bucket) 算法思路简单,水(请求)先进入到漏桶中,漏桶以一定的速度出水(接口有响应速率),当水流速度过大直接溢出(访问频率超过接口响应频率),然后就拒绝请求,可以看出漏桶算法能强制限制数据的传输速率。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图2所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
相关实现算法
参考链接:redis令牌桶实现
**漏桶算法的出水速度是恒定的,**那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。漏桶算法通常可以用于限制访问外部接口的流量,保护其他人系统,比如我们请求银行接口,通常要限制并发数。
令牌桶算法生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,可以处理瞬时流量,而且拿令牌的过程并不是消耗很大的事情。令牌桶算法通常可以用于限制被访问的流量,保护自身系统。
时间窗口,就是以时间间隔维度,统计这个时间段内的数量,和设置的阈值比较,达到限流效果。
如何区分各个时间窗口?
可以将“当前的毫秒数/时间区间“”,那么同一个时间区间计算的结果都是一样的,可以将这个值作为key,用来区分各个时间段。
比如: 用"当前的毫秒数/1000",就表示每一秒,用"当前的毫秒数/5000",就表示每5秒。
测试每一秒:从结果可以发现,同一秒内的毫秒数值,计算后得到的结果是一样的,可以将这个结果作为统计交易的key,用来区分各个时间段。
测试每3秒:可以发现每3秒内的输出值,都是一样的。
再来看限流实现,用Redis + Lua
Lua本身就是一种编程语言,虽然redis 官方没有直接提供限流相应的API,但却支持了 Lua 脚本的功能,可以使用它实现复杂的令牌桶或漏桶算法,也是分布式系统中实现限流的主要方式之一。
相比Redis事务,Lua脚本的优点:
减少网络开销:使用Lua脚本,无需向Redis发送多次请求,执行一次即可,减少网络传输
原子操作:Redis将整个Lua脚本作为一个命令执行,原子,无需担心并发
复用:Lua脚本一旦执行,会永久保存 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中配套的工具类
package sinosoftgz.apiGateway.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.util.Assert;
import java.util.Arrays;
/**
* 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);
// 这里System.currentTimeMillis() / 1000 / timeUnit就是一个时间窗口,比如 5秒 内处理10个请求,timeUnit=5,limit=10
Integer expire = limit + 1;
String convertExpire = isDurable ? "-1" : expire.toString();
return redisTemplate.execute(redisScript, Arrays.asList(key.toString()), limit.toString(), convertExpire).equals(1l);
}
}
优点:时间窗口和令牌桶相比,这种算法不需要去等待令牌生成的时间,在新的时间窗口,可以立即处理大量的请求。
缺点:在一个窗口临界点的前后时间,比如时间窗口是1分钟,在59秒和1分01秒同时突发大量请求,刚好请求全部落在前后相隔很近的节点上,极端情况下可能会带来 2 倍的流量,系统可能承受不了这么大的突发性流量。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。