تقنيات وتطبيق

FastAPI + Redis — كاش، محدد معدل، وقفل معًا

١٣ أكتوبر ٢٠٢٦ · 8 دقائق قراءة

الثلاثة من زاوية واحدة

أكثر ما يتكرر في الـ APIs هو احتياج لحاجات تبدو متفرقة، لكنها كلها من نوع واحد: أمر ذرّي يمسك الحالة الصحيحة في لحظة واحدة. في قالب fastapi-forge خلّيت الثلاثة جنب بعض في core/، وكل واحد بيعتمد إمّا على سطر Lua وحيد أو أمر ذرّي.

1) الكاش

قراءة نتائج عمليات غالية. النقاط الثلاث اللي اتعلمتها بعد ما وقعت فيها:

  • TTL إجباري: نسيان expiry خلّى الذاكرة تنفجر — اجعل default TTL واجبًا في الفانكشن.
  • قراءة/كتابة: get_or_set تعمل fetch فقط لو الكاش فارغ — من غير فوضى round-trips.
  • مسح بالـ namespace: عشان لما يتغير شكل البيانات، تمسح الكاش دفعة واحدة بدل تصفير مفتاح كل سجل بمفرده.

2) محدد المعدل (sliding window)

بدل عدادات بسيطة متقطعة، استخدم سكربت Lua بيحفظ أزمنة الطلبات في ZSET ويعدّ ما داخل النافذة:

local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
ZREMRANGEBYSCORE KEYS[1], 0, now - window
local count = ZCARD KEYS[1]
if count < limit then
  ZADD KEYS[1], now, now
  return 1
end
return 0

هذا تطبيق مباشر لـ «التزامن الأصلي أولًا» — لولا الذرية، محدّد معدل تعمل عليه عمليات متوازية ممكن يكسر قانونه.

3) القفل الموزّع

لأي قسم حرج مشترك بين أكثر من عملية (مثلًا: رندر ملف مشترك مرة واحدة):

async with RedisLock(redis, "orders:42", ttl=30):
    ...

النقطة اللي بتفرّق: الإفراج بتحقق الملكية. عند أخذ القفل بترجع token مميز، والإفراج يقارن القيمة قبل الحذف:

if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
end
return 0

كده ما فيش عملية تفرّج قفل مش بتاعها.

الجمع من غير طبقات زائدة

الثلاثة في core/ يمرّوا على نفس عميل redis.asyncio — بدل مكتبة فوقها مكتبة، استخدمهم كأدوات مستقلة تحت مبدأ «مصدر واحد للحقيقة»: صاحب السياسة واحد، والأمر الذرّي موحّد. ونفس الروح دي في redisjobs — طابور موثوق فوق LMOVE الذرّية.

الخلاصة

Redis مش «كاش فقط» — ذرية أوامره بتسمح بمحدد معدل، وقفل موزّع، وطابور موثوق، كلهم من سكربتات Lua صغيرة أو أوامر الكلمة الواحدة. استخدمهم بلا طبقة زائدة.