我使用INCR
和EXPIRE
来实现速率限制(对于下面的示例,每分钟只允许5个请求):
if EXISTS counter
count = INCR counter
else
EXPIRE counter 60
count = INCR counter
if count > 5
print "Exceeded the limit"
但是有一个问题是,人们可以在最后一秒发送5个请求,在下一分钟发送5个其他请求,换句话说,在2秒内发出10个请求。
有没有更好的方法来避免这个问题?
更新:我刚才提出了一个想法:使用列表来实现它。
times = LLEN counter
if times < 5
LPUSH counter now()
else
time = LINDEX counter -1
if now() - time < 60
print "Exceeded the limit"
else
LPUSH counter now()
LTRIM counter 5
这是一个好方法吗?
答案 0 :(得分:11)
您可以从“最后一分钟的5个请求”切换为“分钟x的5个请求”。通过这种方式可以做到:
counter = current_time # for example 15:03
count = INCR counter
EXPIRE counter 60 # just to make sure redis doesn't store it forever
if count > 5
print "Exceeded the limit"
如果您想继续使用“最后一分钟内的5个请求”,那么您可以
counter = Time.now.to_i # this is Ruby and it returns the number of milliseconds since 1/1/1970
key = "counter:" + counter
INCR key
EXPIRE key 60
number_of_requests = KEYS "counter"*"
if number_of_requests > 5
print "Exceeded the limit"
如果您有生产限制(尤其是性能),则使用KEYS
关键字not advised为{{3}}。我们可以使用 sets :
counter = Time.now.to_i # this is Ruby and it returns the number of milliseconds since 1/1/1970
set = "my_set"
SADD set counter 1
members = SMEMBERS set
# remove all set members which are older than 1 minute
members {|member| SREM member if member[key] < (Time.now.to_i - 60000) }
if (SMEMBERS set).size > 5
print "Exceeded the limit"
这是所有伪Ruby代码,但应该给你一个想法。
答案 1 :(得分:3)
这是一个已经回答的旧问题,但这是我从这里获得灵感的一个实现。我使用ioredis作为Node.js
以下是所有异步但无竞争条件(我希望)荣耀的滚动窗口时间限制器:
var Ioredis = require('ioredis');
var redis = new Ioredis();
// Rolling window rate limiter
//
// key is a unique identifier for the process or function call being limited
// exp is the expiry in milliseconds
// maxnum is the number of function calls allowed before expiry
var redis_limiter_rolling = function(key, maxnum, exp, next) {
redis.multi([
['incr', 'limiter:num:' + key],
['time']
]).exec(function(err, results) {
if (err) {
next(err);
} else {
// unique incremented list number for this key
var listnum = results[0][1];
// current time
var tcur = (parseInt(results[1][1][0], 10) * 1000) + Math.floor(parseInt(results[1][1][1], 10) / 1000);
// absolute time of expiry
var texpiry = tcur - exp;
// get number of transacation in the last expiry time
var listkey = 'limiter:list:' + key;
redis.multi([
['zadd', listkey, tcur.toString(), listnum],
['zremrangebyscore', listkey, '-inf', texpiry.toString()],
['zcard', listkey]
]).exec(function(err, results) {
if (err) {
next(err);
} else {
// num is the number of calls in the last expiry time window
var num = parseInt(results[2][1], 10);
if (num <= maxnum) {
// does not reach limit
next(null, false, num, exp);
} else {
// limit surpassed
next(null, true, num, exp);
}
}
});
}
});
};
这是一种锁定式限速器:
// Lockout window rate limiter
//
// key is a unique identifier for the process or function call being limited
// exp is the expiry in milliseconds
// maxnum is the number of function calls allowed within expiry time
var util_limiter_lockout = function(key, maxnum, exp, next) {
// lockout rate limiter
var idkey = 'limiter:lock:' + key;
redis.incr(idkey, function(err, result) {
if (err) {
next(err);
} else {
if (result <= maxnum) {
// still within number of allowable calls
// - reset expiry and allow next function call
redis.expire(idkey, exp, function(err) {
if (err) {
next(err);
} else {
next(null, false, result);
}
});
} else {
// too many calls, user must wait for expiry of idkey
next(null, true, result);
}
}
});
};
Here's a gist of the functions。如果您发现任何问题,请告诉我。
答案 2 :(得分:3)
进行速率限制的规范方法是通过Leaky bucket algorithm。使用计数器的缺点是,用户可以在计数器重置后立即执行一堆请求,即在下一分钟的第一秒中为您的情况执行5次操作。 Leaky桶算法解决了这个问题。简而言之,您可以使用有序集来存储“漏桶”,使用操作时间戳作为填充它的键。
查看这篇文章的具体实现: Better Rate Limiting With Redis Sorted Sets
<强>更新强>
还有另一种算法,与漏桶相比具有一些优势。它被称为Generic Cell Rate Algorithm。以下是它在更高级别的工作方式,如Rate Limiting, Cells, and GCRA:
中所述GCRA通过称为“理论到达时间”(TAT)的时间跟踪剩余限制来工作,该时间通过将表示其成本的持续时间添加到当前时间而在第一个请求上播种。成本计算为我们的“排放间隔”(T)的乘数,该乘数来自我们希望铲斗重新填充的速率。当任何后续请求进入时,我们采用现有的TAT,从中减去表示限制总突发容量的固定缓冲区(τ+ T),并将结果与当前时间进行比较。此结果表示下次允许请求。如果它在过去,我们允许传入的请求,如果它在将来,我们不会。成功请求后,通过添加T来计算新的TAT。
在GitHub上有一个实现此算法的redis模块:https://github.com/brandur/redis-cell
答案 3 :(得分:2)
注意:以下代码是Java中的示例实现。 私有最终字符串COUNT =“ count”;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private HashOperations hashOperations;
@PostConstruct
private void init() {
hashOperations = stringRedisTemplate.opsForHash();
}
@Override
public boolean isRequestAllowed(String key, long limit, long timeout, TimeUnit timeUnit) {
Boolean hasKey = stringRedisTemplate.hasKey(key);
if (hasKey) {
Long value = hashOperations.increment(key, COUNT, -1l);
return value > 0;
} else {
hashOperations.put(key, COUNT, String.valueOf(limit));
stringRedisTemplate.expire(key, timeout, timeUnit);
}
return true;
}
答案 4 :(得分:1)
你的更新是一个非常好的算法,虽然我做了一些改动:
times = LLEN counter
if times < 5
LPUSH counter now()
else
time = LINDEX counter -1
if now() - time <= 60
print "Exceeded the limit"
else
LPUSH counter now()
RPOP counter
答案 5 :(得分:1)
这是我的leaky bucket
使用Redis Lists
实现速率限制。
注意:以下代码是php
中的示例实现,您可以使用自己的语言来实现。
$list = $redis->lRange($key, 0, -1); // get whole list
$noOfRequests = count($list);
if ($noOfRequests > 5) {
$expired = 0;
foreach ($list as $timestamp) {
if ((time() - $timestamp) > 60) { // Time difference more than 1 min == expired
$expired++;
}
}
if ($expired > 0) {
$redis->lTrim($key, $expired, -1); // Remove expired requests
if (($noOfRequests - $expired) > 5) { // If still no of requests greater than 5, means fresh limit exceeded.
die("Request limit exceeded");
}
} else { // No expired == all fresh.
die("Request limit exceeded");
}
}
$redis->rPush($key, time()); // Add this request as a genuine one to the list, and proceed.
答案 6 :(得分:0)
这是另一种方法。如果目标是在收到第一个请求时从定时器开始限制每Y秒X请求的请求数,那么您可以为要跟踪的每个用户创建2个密钥:一个用于第一个请求的时间收到的是另一个请求的数量。
key = "123"
key_count = "ct:#{key}"
key_timestamp = "ts:#{key}"
if (not redis[key_timestamp].nil?) && (not redis[key_count].nil?) && (redis[key_count].to_i > 3)
puts "limit reached"
else
if redis[key_timestamp].nil?
redis.multi do
redis.set(key_count, 1)
redis.set(key_timestamp, 1)
redis.expire(key_timestamp,30)
end
else
redis.incr(key_count)
end
puts redis[key_count].to_s + " : " + redis[key_timestamp].to_s + " : " + redis.ttl(key_timestamp).to_s
end
答案 7 :(得分:0)
我尝试过LIST,EXPIRE和PTTL
如果tps是每秒5,那么
吞吐量= 5
rampup = 1000(1000ms = 1sec)
间隔= 200ms
local counter = KEYS[1]
local throughput = tonumber(ARGV[1])
local rampUp = tonumber(ARGV[2])
local interval = rampUp / throughput
local times = redis.call('LLEN', counter)
if times == 0 then
redis.call('LPUSH', counter, rampUp)
redis.call('PEXPIRE', counter, rampUp)
return true
elseif times < throughput then
local lastElemTTL = tonumber(redis.call('LINDEX', counter, 0))
local currentTTL = redis.call('PTTL', counter)
if (lastElemTTL-currentTTL) < interval then
return false
else
redis.call('LPUSH', counter, currentTTL)
return true
end
else
return false
end
更简单的版本:
local tpsKey = KEYS[1]
local throughput = tonumber(ARGV[1])
local rampUp = tonumber(ARGV[2])
-- Minimum interval to accept the next request.
local interval = rampUp / throughput
local currentTime = redis.call('PTTL', tpsKey)
-- -2 if the key does not exist, so set an year expiry
if currentTime == -2 then
currentTime = 31536000000 - interval
redis.call('SET', tpsKey, 31536000000, "PX", currentTime)
end
local previousTime = redis.call('GET', tpsKey)
if (previousTime - currentTime) >= interval then
redis.call('SET', tpsKey, currentTime, "PX", currentTime)
return true
else
redis.call('ECHO',"0. ERR - MAX PERMIT REACHED IN THIS INTERVAL")
return false
end
答案 8 :(得分:0)
与其他Java答案类似,但往返Redis的往返次数较少:
@Autowired
private StringRedisTemplate stringRedisTemplate;
private HashOperations hashOperations;
@PostConstruct
private void init() {
hashOperations = stringRedisTemplate.opsForHash();
}
@Override
public boolean isRequestAllowed(String key, long limit, long timeout, TimeUnit timeUnit) {
Long value = hashOperations.increment(key, COUNT, 1l);
if (value == 1) {
stringRedisTemplate.expire(key, timeout, timeUnit);
}
return value > limit;
}
答案 9 :(得分:0)
这足够小,您可以不对它进行散列处理。
local f=redis.call local k=KEYS[1] local a=f('incrby',k,ARGV[1]) local b=f('pttl',k) f('pexpire',k,math.min(b<0 and ARGV[2] or b,ARGV[2])) return a
参数为:
KEYS[1]
=键名,例如可以是限制速率的操作
ARGV[1]
=增量,通常为1,但是您可以在客户端上每10或100毫秒间隔进行批量处理
ARGV[2]
=以秒为单位的时间窗口,
Returns
:新的递增值,然后可以将其与代码中的值进行比较,以查看其是否超出速率限制。
使用此方法不会将ttl设置回基值,它将继续向下滑动直到键到期,这时它将在下一次调用时以ARGV[2]
ttl重新开始。