Redis والتخزين المؤقت المتقدم
تحديد المعدل باستخدام Redis
تحديد المعدل باستخدام Redis
تحديد المعدل ضروري لحماية واجهات برمجة التطبيقات والخدمات من سوء الاستخدام، وضمان الاستخدام العادل، والحفاظ على استقرار النظام. Redis مثالي لتحديد المعدل الموزع نظراً لعملياته الذرية والكمون المنخفض.
لماذا تحديد المعدل؟
حالات الاستخدام الشائعة:
✓ تقييد API - تحديد الطلبات لكل مستخدم/IP ✓ حماية محاولات تسجيل الدخول - منع هجمات القوة الغاشمة ✓ حماية الموارد - تحديد العمليات المكلفة ✓ فرض الاستخدام العادل - ضمان الوصول العادل ✓ تخفيف DDoS - الحماية من فيضانات حركة المرور ✓ التحكم في التكلفة - تحديد استخدام واجهات برمجة التطبيقات الخارجية المدفوعة
تحديد المعدل بالنافذة الثابتة
أبسط خوارزمية: السماح بـ N طلبات لكل نافذة زمنية ثابتة (مثل 100 طلب في الدقيقة):
التنفيذ:
use Illuminate\Support\Facades\Redis;
class FixedWindowRateLimiter
{
public function attempt(string $key, int $limit, int $windowSeconds): bool
{
$currentWindow = floor(time() / $windowSeconds);
$redisKey = "rate_limit:{$key}:{$currentWindow}";
// زيادة العداد
$count = Redis::incr($redisKey);
// تعيين انتهاء الصلاحية عند أول طلب
if ($count === 1) {
Redis::expire($redisKey, $windowSeconds * 2);
}
return $count <= $limit;
}
public function getRemainingAttempts(string $key, int $limit, int $windowSeconds): int
{
$currentWindow = floor(time() / $windowSeconds);
$redisKey = "rate_limit:{$key}:{$currentWindow}";
$count = (int) Redis::get($redisKey) ?? 0;
return max(0, $limit - $count);
}
}
// الاستخدام
$rateLimiter = new FixedWindowRateLimiter();
$userId = auth()->id();
if ($rateLimiter->attempt("user:{$userId}", 100, 60)) {
// السماح بالطلب
return $this->processApiRequest();
} else {
return response()->json(['error' => 'تم تجاوز حد المعدل'], 429);
}مشكلة النافذة الثابتة: تسمح بانفجار حركة المرور عند حدود النافذة. مثال: 100 طلب في 00:59، ثم 100 طلب آخر في 01:00 = 200 طلب في ثانية واحدة!
تحديد المعدل بالنافذة المنزلقة
خوارزمية أكثر دقة تمنع انفجارات الحدود من خلال النظر في نافذة زمنية متدحرجة:
النافذة المنزلقة بالمجموعات المرتبة:
class SlidingWindowRateLimiter
{
public function attempt(string $key, int $limit, int $windowSeconds): bool
{
$now = microtime(true);
$windowStart = $now - $windowSeconds;
$redisKey = "rate_limit:sliding:{$key}";
// إزالة الإدخالات القديمة خارج النافذة
Redis::zremrangebyscore($redisKey, 0, $windowStart);
// عد الإدخالات الحالية في النافذة
$count = Redis::zcard($redisKey);
if ($count < $limit) {
// إضافة الطابع الزمني الحالي
Redis::zadd($redisKey, $now, $now);
Redis::expire($redisKey, $windowSeconds + 1);
return true;
}
return false;
}
public function getRemainingAttempts(string $key, int $limit, int $windowSeconds): int
{
$now = microtime(true);
$windowStart = $now - $windowSeconds;
$redisKey = "rate_limit:sliding:{$key}";
Redis::zremrangebyscore($redisKey, 0, $windowStart);
$count = Redis::zcard($redisKey);
return max(0, $limit - $count);
}
public function getResetTime(string $key, int $windowSeconds): int
{
$redisKey = "rate_limit:sliding:{$key}";
// الحصول على أقدم طابع زمني
$oldest = Redis::zrange($redisKey, 0, 0, 'WITHSCORES');
if (empty($oldest)) {
return time();
}
$oldestTime = array_values($oldest)[0];
return (int) ceil($oldestTime + $windowSeconds);
}
}المزايا: منع انفجارات الحدود، تحديد معدل أكثر دقة، توزيع حركة المرور بسلاسة. العيب: يستخدم المزيد من الذاكرة (يخزن الطوابع الزمنية).
خوارزمية دلو الرمز المميز
خوارزمية متقدمة تسمح بالانفجارات مع الحفاظ على المعدل المتوسط:
تنفيذ دلو الرمز المميز:
class TokenBucketRateLimiter
{
private int $capacity; // الحد الأقصى للرموز
private float $refillRate; // رموز في الثانية
public function __construct(int $capacity, float $refillRate)
{
$this->capacity = $capacity;
$this->refillRate = $refillRate;
}
public function attempt(string $key, int $tokens = 1): bool
{
$redisKey = "rate_limit:bucket:{$key}";
$now = microtime(true);
// الحصول على الحالة الحالية
$data = Redis::get($redisKey);
if ($data) {
$state = json_decode($data, true);
$lastRefill = $state['last_refill'];
$currentTokens = $state['tokens'];
} else {
$lastRefill = $now;
$currentTokens = $this->capacity;
}
// حساب الرموز لإضافتها بناءً على الوقت المنقضي
$elapsed = $now - $lastRefill;
$tokensToAdd = $elapsed * $this->refillRate;
$currentTokens = min($this->capacity, $currentTokens + $tokensToAdd);
// التحقق من توفر رموز كافية
if ($currentTokens >= $tokens) {
$currentTokens -= $tokens;
$lastRefill = $now;
// حفظ الحالة
Redis::setex($redisKey, 3600, json_encode([
'tokens' => $currentTokens,
'last_refill' => $lastRefill
]));
return true;
}
return false;
}
public function getAvailableTokens(string $key): float
{
$redisKey = "rate_limit:bucket:{$key}";
$data = Redis::get($redisKey);
if (!$data) {
return $this->capacity;
}
$state = json_decode($data, true);
$elapsed = microtime(true) - $state['last_refill'];
$tokensToAdd = $elapsed * $this->refillRate;
return min($this->capacity, $state['tokens'] + $tokensToAdd);
}
}
// الاستخدام - السماح بالانفجارات حتى 100 طلب، إعادة التعبئة بـ 10/ثانية
$rateLimiter = new TokenBucketRateLimiter(100, 10);
if ($rateLimiter->attempt("user:{$userId}", 1)) {
return $this->processApiRequest();
} else {
return response()->json(['error' => 'تم تجاوز حد المعدل'], 429);
}فوائد دلو الرمز المميز: يسمح بانفجارات محكومة (تجربة مستخدم جيدة)، يحافظ على المعدل المتوسط بمرور الوقت، مرن (يمكن أن تكلف العمليات المختلفة رموزاً مختلفة).
middleware تحديد المعدل في Laravel
دمج تحديد المعدل في مسارات Laravel الخاصة بك:
تنفيذ Middleware:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class RateLimitMiddleware
{
private SlidingWindowRateLimiter $limiter;
public function __construct()
{
$this->limiter = new SlidingWindowRateLimiter();
}
public function handle(Request $request, Closure $next, int $limit = 60, int $window = 60)
{
$key = $this->resolveRequestIdentifier($request);
if (!$this->limiter->attempt($key, $limit, $window)) {
return response()->json([
'error' => 'طلبات كثيرة جداً',
'retry_after' => $this->limiter->getResetTime($key, $window)
], 429)
->header('X-RateLimit-Limit', $limit)
->header('X-RateLimit-Remaining', 0)
->header('Retry-After', $this->limiter->getResetTime($key, $window));
}
$remaining = $this->limiter->getRemainingAttempts($key, $limit, $window);
return $next($request)
->header('X-RateLimit-Limit', $limit)
->header('X-RateLimit-Remaining', $remaining);
}
private function resolveRequestIdentifier(Request $request): string
{
if ($user = $request->user()) {
return "user:{$user->id}";
}
return "ip:{$request->ip()}";
}
}
// التسجيل في app/Http/Kernel.php
protected $routeMiddleware = [
'throttle.custom' => \App\Http\Middleware\RateLimitMiddleware::class,
];
// الاستخدام في المسارات
Route::middleware(['throttle.custom:100,60'])->group(function () {
Route::get('/api/users', [UserController::class, 'index']);
Route::post('/api/users', [UserController::class, 'store']);
});تحديد المعدل الموزع
ضمان عمل حدود المعدل عبر خوادم تطبيقات متعددة:
نص Lua للعمليات الذرية:
class DistributedRateLimiter
{
public function attempt(string $key, int $limit, int $window): bool
{
$script = <<<LUA
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window_start = now - window
-- إزالة الإدخالات القديمة
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
-- عد الإدخالات الحالية
local count = redis.call('ZCARD', key)
if count < limit then
-- إضافة إدخال جديد
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window + 1)
return 1
else
return 0
end
LUA;
$now = microtime(true);
$result = Redis::eval($script, 1, "rate_limit:{$key}", $limit, $window, $now);
return $result === 1;
}
}
// هذا يضمن العمليات الذرية عبر خوادم متعددةحرج للأنظمة الموزعة: استخدام نصوص Lua يضمن أن جميع عمليات تحديد المعدل ذرية، مما يمنع ظروف السباق عندما تتحقق خوادم متعددة من الحدود في وقت واحد.
تحديد المعدل المبني على IP مقابل المبني على المستخدم
استراتيجية تحديد المعدل المرنة:
class AdaptiveRateLimiter
{
private array $limits = [
'anonymous' => ['limit' => 10, 'window' => 60],
'authenticated' => ['limit' => 100, 'window' => 60],
'premium' => ['limit' => 1000, 'window' => 60],
];
public function checkLimit(Request $request): bool
{
$tier = $this->getUserTier($request);
$config = $this->limits[$tier];
$key = $this->generateKey($request, $tier);
$limiter = new SlidingWindowRateLimiter();
return $limiter->attempt($key, $config['limit'], $config['window']);
}
private function getUserTier(Request $request): string
{
if (!$request->user()) {
return 'anonymous';
}
if ($request->user()->isPremium()) {
return 'premium';
}
return 'authenticated';
}
private function generateKey(Request $request, string $tier): string
{
if ($tier === 'anonymous') {
return "ip:{$request->ip()}";
}
return "user:{$request->user()->id}";
}
}أنماط حد المعدل لنقاط نهاية مختلفة
حدود خاصة بالمسار:
// حدود صارمة للعمليات المكلفة
Route::post('/api/export', [ExportController::class, 'export'])
->middleware('throttle.custom:5,3600'); // 5 في الساعة
// حدود معتدلة لاستدعاءات API العادية
Route::get('/api/products', [ProductController::class, 'index'])
->middleware('throttle.custom:100,60'); // 100 في الدقيقة
// حدود متساهلة للبيانات العامة
Route::get('/api/public/categories', [CategoryController::class, 'index'])
->middleware('throttle.custom:300,60'); // 300 في الدقيقة
// صارم جداً للمصادقة
Route::post('/api/login', [AuthController::class, 'login'])
->middleware('throttle.custom:5,300'); // 5 في 5 دقائقمراقبة حد المعدل والتنبيهات
تتبع انتهاكات حد المعدل:
class RateLimitMonitor
{
public function recordViolation(string $key, Request $request)
{
$violationKey = "rate_limit:violations:{$key}";
Redis::hincrby($violationKey, date('Y-m-d:H'), 1);
Redis::expire($violationKey, 86400 * 7); // الاحتفاظ لـ 7 أيام
// التنبيه إذا تم تجاوز العتبة
$hourlyCount = Redis::hget($violationKey, date('Y-m-d:H'));
if ($hourlyCount > 100) {
$this->sendAlert($key, $hourlyCount, $request);
}
}
public function getViolationStats(string $key): array
{
$violationKey = "rate_limit:violations:{$key}";
return Redis::hgetall($violationKey) ?? [];
}
private function sendAlert(string $key, int $count, Request $request)
{
logger()->warning("انتهاكات عالية لحد المعدل", [
'key' => $key,
'count' => $count,
'ip' => $request->ip(),
'user_agent' => $request->userAgent()
]);
// الإرسال إلى خدمة المراقبة (مثل Sentry، Slack)
}
}أفضل الممارسات
إرشادات تحديد المعدل:
1. اختر الخوارزمية المناسبة: ✓ النافذة الثابتة - بسيطة، ذاكرة منخفضة ✓ النافذة المنزلقة - دقيقة، تمنع الانفجارات ✓ دلو الرمز المميز - يسمح بالانفجارات، أفضل تجربة مستخدم 2. تعيين حدود مناسبة: ✓ المصادقة: 5-10 لكل 5-15 دقيقة ✓ قراءات API: 100-1000 في الدقيقة ✓ كتابات API: 50-100 في الدقيقة ✓ عمليات مكلفة: 5-10 في الساعة 3. إرجاع رؤوس مناسبة: ✓ X-RateLimit-Limit (المسموح به الإجمالي) ✓ X-RateLimit-Remaining (المتبقي) ✓ X-RateLimit-Reset (طابع زمني لإعادة الضبط) ✓ Retry-After (ثوانٍ للانتظار) 4. استخدم حدود متدرجة: ✓ مجهول < مصادق < مميز ✓ حدود مختلفة لكل نقطة نهاية ✓ التعديل بناءً على سلوك المستخدم 5. المراقبة والتنبيه: ✓ تتبع أنماط الانتهاكات ✓ التنبيه عند الارتفاعات غير العادية ✓ التحليل للهجمات المحتملة ✓ تعديل الحدود بناءً على البيانات
تمرين عملي:
- تنفيذ جميع خوارزميات تحديد المعدل الثلاثة (النافذة الثابتة، النافذة المنزلقة، دلو الرمز المميز)
- إنشاء middleware يطبق حدود معدل متدرجة (مجهول: 10/دقيقة، مصادق: 100/دقيقة، مميز: 1000/دقيقة)
- بناء لوحة تحكم مراقبة تعرض انتهاكات حد المعدل حسب الساعة/اليوم
- تنفيذ حدود معدل لكل نقطة نهاية (تسجيل دخول: 5/5دقائق، تصدير: 3/ساعة، API: 100/دقيقة)
- إضافة تنفيذ نص Lua لتحديد المعدل الموزع لمنع ظروف السباق
- إنشاء نقطة نهاية مسؤول لعرض وتعديل حدود المعدل مؤقتاً لمستخدمين محددين