hkeys - redis instructions
Cómo implementar la limitación de velocidad usando Redis (8)
Utilizo INCR
y EXPIRE
para implementar la limitación de velocidad (para el ejemplo a continuación, solo permito 5 solicitudes por minuto):
if EXISTS counter
count = INCR counter
else
EXPIRE counter 60
count = INCR counter
if count > 5
print "Exceeded the limit"
Pero existe el problema de que las personas pueden enviar 5 solicitudes en el último segundo en un minuto y otras 5 solicitudes en el primer segundo en el siguiente minuto, es decir, 10 solicitudes en dos segundos.
¿Hay alguna forma mejor de evitar el problema?
Actualización: se me ocurrió una idea en este momento: usar una lista para implementarla.
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
¿Es una buena manera?
Aquí está mi implementación de la limitación de velocidad, utilizando las Lists
Redis.
Nota: el siguiente código es una implementación de ejemplo en php
, puede implementarlo en su propio idioma.
$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.
Aquí hay un enfoque alternativo. Si el objetivo es limitar el número de solicitudes a X solicitudes por Y segundos con el temporizador comenzando cuando se recibe la primera solicitud, entonces puede crear 2 claves para cada usuario que desea rastrear: una para el momento en que la primera solicitud Se recibió y otro por el número de solicitudes realizadas.
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
Esta es una vieja pregunta que ya fue respondida, pero aquí hay una implementación que tomé un poco de inspiración de aquí. Estoy usando ioredis para Node.js
Aquí está el limitador de tiempo de la ventana móvil en toda su gloria asíncrona pero sin condiciones de carrera (espero):
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);
}
}
});
}
});
};
Y aquí hay un tipo de limitador de tasa de bloqueo:
// 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);
}
}
});
};
Aquí hay un resumen de las funciones . Déjame saber si ves algún problema.
He probado con LIST, EXPIRE y PTTL
Si tps es 5 por segundo, entonces
rendimiento = 5
rampup = 1000 (1000ms = 1sec)
intervalo = 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
Más versión más simple:
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
La forma canónica de realizar la limitación de velocidad es a través del algoritmo Leaky bucket . La desventaja de usar un contador, es que un usuario puede realizar una gran cantidad de solicitudes justo después de que se reinicie el contador, es decir, 5 acciones en el primer segundo del siguiente minuto para su caso. El algoritmo del cubo Leaky resuelve este problema. Brevemente, puede usar conjuntos ordenados para almacenar su "cubeta con fugas", usando sellos de tiempo de acción como claves para llenarlo.
Echa un vistazo a este artículo para conocer la implementación exacta: Mejor límite de velocidad con Redis conjuntos ordenados
ACTUALIZAR:
También hay otro algoritmo, que tiene algunas ventajas en comparación con el cubo con fugas. Se llama algoritmo genérico de velocidad celular . Así es como funciona en el nivel superior, como se describe en Limitación de velocidad, Celdas y GCRA :
GCRA funciona al rastrear el límite restante a través de un tiempo llamado "tiempo de llegada teórico" (TAT), que se agrega a la primera solicitud al agregar una duración que representa su costo a la hora actual. El costo se calcula como un multiplicador de nuestro "intervalo de emisión" (T), que se deriva de la tasa a la que queremos que se llene el cubo. Cuando llega una solicitud posterior, tomamos el TAT existente, le restamos un búfer fijo que representa la capacidad de ráfaga total del límite (τ + T) y comparamos el resultado con la hora actual. Este resultado representa la próxima vez para permitir una solicitud. Si está en el pasado, permitimos la solicitud entrante, y si está en el futuro, no lo hacemos. Después de una solicitud exitosa, se calcula un TAT nuevo agregando T.
Hay un módulo redis que implementa este algoritmo disponible en GitHub: https://github.com/brandur/redis-cell
Nota: El siguiente código es una implementación de ejemplo en Java. Cadena final privada 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;
}
Puede cambiar de "5 solicitudes en el último minuto" a "5 solicitudes en el minuto x". Por esto sería posible hacer:
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"
Si desea seguir usando "5 solicitudes en el último minuto", entonces podría hacer
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"
Si tiene restricciones de producción (especialmente el rendimiento), no se recomienda utilizar la palabra clave KEYS
. Podríamos usar conjuntos en su lugar:
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"
Todo esto es un código pseudo Ruby, pero debería darle una idea.
Su actualización es un algoritmo muy bueno, aunque hice algunos cambios:
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