最近项目需要实现限流的功能,项目使用的是spring cloud框架,用zuul做网管模块。准备在网管层加上限流功能。
1、使用RateLimiter+filter做统一入口限流。适用单机
Guava中开源出来一个令牌桶算法的工具类RateLimiter,使用简单,cloud已经集成该模块,直接引入。
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
直接新建一个zuulFilter,类型 pre.
@Component
public class RateLimitZuulFilter extends ZuulFilter{
private static final Logger LOGGER = LoggerFactory.getLogger(RateLimitZuulFilter.class);
//初始化 放入 1000令牌/s 时间窗口为 1s
private final RateLimiter rateLimiter = RateLimiter.create(1000.0);
@Override
public boolean shouldFilter() {
// 一直过滤
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse response = ctx.getResponse();
if(!rateLimiter.tryAcquire()) {
response.setContentType(MediaType.TEXT_PLAIN_VALUE);
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
ctx.setSendZuulResponse(false);// 过滤该请求,不对其进行路由
try {
response.getWriter().write("TOO MANY REQUESTS");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
ctx.setResponseStatusCode(200);
LOGGER.info("OK !!!");
}
return null;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return -5;
}
}
2、使用zuul + RateLimiter 配置方式,在application.yml加简单配置就行,适用分布式
zuul:
add-host-header: true
routes:
servicewel:
path: /getway/servicewel/**
serviceId: service-wel
servicehi:
path: /getway/servicehi/**
serviceId: service-hi
ratelimit:
enabled: true
behind-proxy: true
key-prefix: ilea-getway-key
repository: Redis
policies:
servicewel:
limit: 5
quota: 30
refresh-interval: 60
type:
- URL
- USER
- ORIGIN
servicehi:
limit: 10
quota: 30
refresh-interval: 60
type:
- URL
- USER
- repository :是key值保存方式,可以选Redis、Consul、Spring Data JPA等方式,这里选择的是 Redis,所以要添加redis依赖和配置。
- limit 单位时间内允许访问的次数
- quota 单位时间内允许访问的总时间(单位时间窗口期内,所有的请求的总时间不能超过这个时间限制)
- refresh-interval 单位时间设置
- type 限流类型:
- url类型的限流就是通过请求路径区分
- origin是通过客户端IP地址区分
- user是通过登录用户名进行区分,也包括匿名用户
通过用户名进行限流可以自定义key策略
@Bean
public RateLimitKeyGenerator rateLimitKeyGenerator(final RateLimitProperties properties,final RateLimitUtils rateLimitUtils) {
//RateLimitPreFilter
return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils) {
@Override
public String key(final HttpServletRequest request, final Route route, final Policy policy) {
String name = request.getParameter("name");
return super.key(request, route, policy)+":"+name;
}
};
}
3、redis计数器限流,利用redis.incrBy()方法实现计数器,最简单的实现方法,无法实现平滑。适用于分布式
public synchronized boolean access() {
if(!redis.hasKey(COUNTER_KEY)) {
redis.set(COUNTER_KEY,1,(long)2, TimeUnit.SECONDS);//时间到就重新初始化
return true;
}
if(reids.hasKey(COUNTER_KEY)&&redsi.incrBy(COUNTER_KEY,(long)1) > (long)400) {
LOGGER.info("调用频率过快");
return false;
}
return true;
}
// LUA
local key = KEYS[1] --count_key
local limit = tonumber(ARGV[1]) --limit
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置2秒过期
redis.call("INCRBY", key,"1")
redis.call("expire", key,"2")
return 1
end
4、代码实现令牌桶算法
参考https://zhuanlan.zhihu.com/p/20872901 ,大神讲的很仔细
可以在 Bucket 中存放现在的 Token 数量,然后存储上一次补充 Token 的时间戳,当用户下一次请求获取一个 Token 的时候, 根据此时的时间戳,计算从上一个时间戳开始,到现在的这个时间点所补充的所有 Token 数量,加入到 Bucket 当中。
public class RateLimiter {
private JedisPool jedisPool;
private long intervalInMills;
private long limit;
private double intervalPerPermit;
public RateLimiter() {
jedisPool = new JedisPool("127.0.0.1", 6379);
intervalInMills = 10000;
limit = 3;
intervalPerPermit = intervalInMills * 1.0 / limit;
}
// 单线程操作下才能保证正确性
// 需要这些操作原子性的话,最好使用 redis 的 lua script
public boolean access(String userId) {
String key = genKey(userId);
try (Jedis jedis = jedisPool.getResource()) {
// 取桶
Map<String, String> counter = jedis.hgetAll(key);
if (counter.size() == 0) {
TokenBucket tokenBucket = new TokenBucket(System.currentTimeMillis(), limit - 1);
jedis.hmset(key, tokenBucket.toHash());
return true;
} else {
TokenBucket tokenBucket = TokenBucket.fromHash(counter);
//取上次添加令牌时间,求与当前时间差值,计算是否加令牌,加多少
long lastRefillTime = tokenBucket.getLastRefillTime();
long refillTime = System.currentTimeMillis();
long intervalSinceLast = refillTime - lastRefillTime;
long currentTokensRemaining;
if (intervalSinceLast > intervalInMills) {
//差值大于 周期, 令牌设为最大
currentTokensRemaining = limit;
} else {
// 根据 添加令牌速率计算应该添加多少令牌
long grantedTokens = (long) (intervalSinceLast / intervalPerPermit);
currentTokensRemaining = Math.min(grantedTokens + tokenBucket.getTokensRemaining(), limit);
}
tokenBucket.setLastRefillTime(refillTime);
assert currentTokensRemaining >= 0;
if (currentTokensRemaining == 0) {
//无令牌可用
tokenBucket.setTokensRemaining(currentTokensRemaining);
jedis.hmset(key, tokenBucket.toHash());
return false;
} else {
//使用一个令牌
tokenBucket.setTokensRemaining(currentTokensRemaining - 1);
jedis.hmset(key, tokenBucket.toHash());
return true;
}
}
}
}
private String genKey(String userId) {
return "rate:limiter:" + intervalInMills + ":" + limit + ":" + userId;
}
public static class TokenBucket {
/*
* 上一次添加时间戳
*/
private long lastRefillTime;
/*
* 剩下的令牌数
*/
private long tokensRemaining;
public TokenBucket(long lastRefillTime, long tokensRemaining) {
this.lastRefillTime = lastRefillTime;
this.tokensRemaining = tokensRemaining;
}
public static TokenBucket fromHash(Map<String, String> hash) {
long lastRefillTime = Long.parseLong(hash.get("lastRefillTime"));
int tokensRemaining = Integer.parseInt(hash.get("tokensRemaining"));
return new TokenBucket(lastRefillTime, tokensRemaining);
}
public Map<String, String> toHash() {
Map<String, String> hash = new HashMap<>();
hash.put("lastRefillTime", String.valueOf(lastRefillTime));
hash.put("tokensRemaining", String.valueOf(tokensRemaining));
return hash;
}
}
}
5、LUA+redis
local key, intervalPerPermit, refillTime = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2])
local limit, interval = tonumber(ARGV[3]), tonumber(ARGV[4])
local bucket = redis.call('hgetall', key)
local currentTokens
-- table.maxn(bucket) 不存在 key值为正数的值 = 0,即bucket不存在
if table.maxn(bucket) == 0 then
-- 设置令牌数为最大
currentTokens = limit
redis.call('hset', key, 'lastRefillTime', refillTime)
elseif table.maxn(bucket) == 4 then
-- 桶存在,先计算需要添加的令牌
local lastRefillTime, tokensRemaining = tonumber(bucket[2]), tonumber(bucket[4])
if refillTime > lastRefillTime then
-- 计算差值
-- 1.过了整个周期了,需要补到最大值
-- 2.如果到了至少补充一个的周期了,那么需要补充部分,否则不补充
local intervalSinceLast = refillTime - lastRefillTime
if intervalSinceLast > interval then
currentTokens = limit
redis.call('hset', key, 'lastRefillTime', refillTime)
else
local grantedTokens = math.floor(intervalSinceLast / intervalPerPermit)
if grantedTokens > 0 then
-- ajust lastRefillTime, we want shift left the refill time.
local padMillis = math.fmod(intervalSinceLast, intervalPerPermit)
redis.call('hset', key, 'lastRefillTime', refillTime - padMillis)
end
currentTokens = math.min(grantedTokens + tokensRemaining, limit)
end
else
-- 有别的线程已添加过
currentTokens = tokensRemaining
end
end
assert(currentTokens >= 0)
if currentTokens == 0 then
-- 无令牌可用
redis.call('hset', key, 'tokensRemaining', currentTokens)
return 0
else
redis.call('hset', key, 'tokensRemaining', currentTokens - 1)
return 1
end
java中判断
public boolean access(String userId) {
String key = genKey(userId);
/**
* keys[1] = key;
* arvg[1] = intervalPerPermit; 每个用多少秒
* arvg[2] = System.currentTimeMillis() 当前时间
* avrg[3] = 总令牌数
* avrg[4] = 周期
*/
long result = (long) jedis.evalsha(scriptSha1, 1, key,
String.valueOf(intervalPerPermit),
String.valueOf(System.currentTimeMillis()),
String.valueOf(limit),
String.valueOf(intervalInMills));
return result == 1L;
}
参照博客:
https://zhuanlan.zhihu.com/p/20872901
https://blog.csdn.net/lsblsb/article/details/69486012
这两篇博客写的非常棒,给我很大帮助。谢谢大神。