Redis 非常适合限速实现,例如:
我们需要每个用户唯一的密钥,这里我们使用会话密钥或用户的 ip 地址 + 用户代理字符串的哈希值,但如果您想全局限制速率,则返回一个常量(例如 @ 987654321@ 参数)会这样做:
import hashlib
def _ratelimit_key(servicename, request):
"""Return a key that identifies one visitor uniquely and is durable.
"""
sesskey = request.session.session_key
if sesskey is not None:
unique = sesskey
else:
ip = request.get_host()
ua = request.META.get('HTTP_USER_AGENT', 'no-user-agent')
digest = hashlib.md5(ua).hexdigest()
unique = '%s-%s' % (ip, digest)
return '%s-%s' % (servicename, unique)
那么速率限制器可以实现为装饰器:
import time
from django.conf import settings
from django import http
import redis
def strict_ratelimit(name, seconds=10, message="Too many requests."):
"""Basic rate-limiter, only lets user through 10 seconds after last attempt.
Args:
name: the service to limit (in case several views share a service)
seconds: the length of the quiet period
message: the message to display to the user when rate limited
"""
def decorator(fn):
def wrap(request, *args, **kwargs):
r = redis.Redis()
key = _ratelimit_key(name, request)
if r.exists(key):
r.expire(key, seconds) # refresh timeout
return http.HttpResponse(message, status=409)
r.setex(key, seconds, "nothing")
return fn(request, *args, **kwargs)
return wrap
return decorator
用法:
@strict_ratelimit('search', seconds=5)
def my_search_view(request):
...
严格的速率限制器通常不是您想要的,但是,通常您希望允许人们有小的突发(只要在一个时间间隔内没有太多)。
“漏桶”(google it)算法可以做到这一点(与上面的用法相同):
def leaky_bucket(name, interval=30, size=3, message="Too many request."):
"""Rate limiter that allows bursts.
Args:
name: the service to limit (several views can share a service)
interval: the timperiod (in seconds)
size: maximum number of activities in a timeperiod
message: message to display to the user when rate limited
"""
def decorator(fn):
def wrap(request, *args, **kwargs):
r = redis.Redis()
key = _ratelimit_key(name, request)
if r.exists(key):
val = r.hgetall(key)
value = float(val['value'])
now = time.time()
# leak the bucket
elapsed = now - float(val['timestamp'])
value -= max(0.0, elapsed / float(interval) * size)
if value + 1 > size:
r.hmset(key, dict(timestamp=now, value=value))
r.expire(key, interval)
return http.HttpResponse(message, status=409)
else:
value += 1.0
r.hmset(key, dict(timestamp=now, value=value))
r.expire(key, interval)
return fn(request, *args, **kwargs)
else:
r.hmset(key, dict(timestamp=time.time(), value=1.0))
r.expire(key, interval)
return fn(request, *args, **kwargs)
return wrap
return decorator