INCR
とEXPIRE
を使用してレート制限を実装します(以下の例では、1分あたり5つのリクエストのみを許可します)。
if EXISTS counter
count = INCR counter
else
EXPIRE counter 60
count = INCR counter
if count > 5
print "Exceeded the limit"
しかし、人々が1分間に最後の1秒間に5つのリクエストを送信し、次の1分間に最初の1秒間に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
それは良い方法ですか?
「最後の1分間に5つのリクエスト」から「1分間に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"
「最後の1分間に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
キーワードを使用することをお勧めします。代わりに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秒間に5つのアクションを実行できます。リーキーバケットアルゴリズムはこの問題を解決します。簡単に言うと、注文したセットを使用して「リーキーバケット」を保存し、アクションタイムスタンプをキーとして使用して埋めることができます。
正確な実装については、この記事を確認してください: Redisソートセットによるレート制限の改善
更新:
リーキーバケットと比較していくつかの利点がある別のアルゴリズムもあります。それは ジェネリックセルレートアルゴリズム と呼ばれます。 レート制限、セル、およびGCRA で説明されているように、より高いレベルでどのように機能するかを次に示します。
GCRAは、「理論的到着時間」(TAT)と呼ばれる時間を通して残りの制限を追跡することによって機能します。これは、現在の時間にコストを表す期間を追加することによって、最初の要求にシードされます。コストは、「排出間隔」(T)の乗数として計算されます。これは、バケットに補充する速度から導き出されます。後続のリクエストが来ると、既存のTATを取得し、制限の合計バースト容量を表す固定バッファー(τ+ T)を減算して、結果を現在の時刻と比較します。この結果は、次にリクエストを許可する時間を表します。過去の場合は受信リクエストを許可し、将来の場合は許可しません。リクエストが成功すると、Tを追加して新しいTATが計算されます。
このアルゴリズムを実装するredisモジュールがGitHubで利用可能です: https://github.com/brandur/redis-cell
これはすでに回答済みの古い質問ですが、ここからインスピレーションを得て行った実装を次に示します。 Node.jsに ioredis を使用しています
これは、非同期でありながら競合状態のない(私が望む)栄光のローリングウィンドウタイムリミッターです。
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);
}
}
});
};
ここに関数の要点があります 。問題が発生した場合はお知らせください。
注:次のコードは、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;
}
他の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;
}
これが私の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.
私はいくつかの変更を加えましたが、あなたの更新は非常に素晴らしいアルゴリズムです。
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
LIST、EXPIRE、PTTLで試しました
Tpsが1秒あたり5の場合、
スループット= 5
rampup = 1000(1000ms = 1秒)
間隔= 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
これが別のアプローチです。目標が、最初のリクエストを受信したときにタイマーを開始して、Y秒あたりのリクエスト数をXリクエストに制限することである場合、追跡するユーザーごとに2つのキーを作成できます。1つは最初のリクエストの時間用です。が受信され、別のリクエスト数が送信されました。
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
これは十分に小さいので、ハッシュしないで逃げることができます。
local f,k,a,b f=redis.call k=KEYS[1] a=f('incrby',k,ARGV[1]) b=f('pttl',k) if b<0 then f('pexpire',k,ARGV[2]) end return a
パラメータは次のとおりです。
KEYS[1]
=キー名、たとえばレート制限へのアクションである可能性がありますARGV[1]
=増分する量、通常は1ですが、クライアントでは10または100ミリ秒間隔でバッチ処理できます。ARGV[2]
=ウィンドウ(ミリ秒単位)、レート制限Returns
:新しい増分値。コード内の値と比較して、レート制限を超えているかどうかを確認できます。
Ttlは、このメソッドでは基本値に戻されません。キーの有効期限が切れるまで下にスライドし続けます。有効期限が切れると、次の呼び出しでARGV[2]
ttlからやり直します。