أغلب الكلام عن أداء Django بيروح على طول لتحسين الـ queries على قاعدة البيانات — إضافة indexes، واستخدام select_related، وتجنّب مشاكل الـ N+1. ده مهم، بس فيه طبقة فوقه غالبًا بيبقى تأثيرها أكبر وتقدروا تنفّذوها بسرعة: الـ caching. الـ caching framework المدمج في Django ناضج ومرن وبيشتغل مع أي backend. السؤال هو إمتى وإزاي نستخدمه.
مثلًا، في لوحات التحكم الخاصة بالمؤثرين في Twiscope، حل مشكلة endpoint بطيء بيحسب aggregates كان مزيج من composite indexes و response cache على Redis لكل مستخدم مع TTLs قصيرة — من غير إعادة تصميم للـ schema. الأجزاء الجاية بتشرح طرق استخدام الـ framework وإمتى نختار كل طريقة.
الـ cache backends في Django
Django بيدعم أكتر من cache backend جاهز من غير أي إضافات. الاختيار الصح بيعتمد على الـ infrastructure والمتطلبات عندكم.
- LocMemCache — cache في الذاكرة جوه الـ process نفسه، سريع بس خاص بكل process ومش متشارك بين الـ workers. ينفع للتطوير بس.
- FileBasedCache — بيخزن الـ cache على الـ disk. مبيضيعش مع الـ restart، بس بطيء. وقليل لما يكون الاختيار الصح لتطبيقات الويب.
- PyMemcacheCache / PyLibMCCache — سريعين وموزّعين ومعمولين مخصوص للـ caching. مفيش persistence ومفيش data structures معقدة. (الـ backend القديم MemcachedCache اتشال في Django 4.1.)
- RedisCache — موجود جوه Django من إصدار 4.0 (django.core.cache.backends.redis.RedisCache)؛ و django-redis، الـ package اللي من طرف تالت، بتضيف حاجات زيادة زي delete_pattern. ده الاختيار المعتاد لتطبيقات Django على الـ production.
# settings.py — Redis cache configuration
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"SOCKET_CONNECT_TIMEOUT": 5,
"SOCKET_TIMEOUT": 5,
"IGNORE_EXCEPTIONS": True, # degrade gracefully if Redis is down
},
"KEY_PREFIX": "myapp",
"TIMEOUT": 300, # 5 minutes default TTL
}
}المستوى 1: cache للـ view كله
أبسط بداية إنكم تعملوا cache للـ response بتاع الـ view كله. لو الـ view بيرجّع نفس الـ response لأي مستخدم مش عامل login خلال فترة زمنية معينة، اعملوا cache للـ response كله.
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.views import View
# Function-based view
@cache_page(60 * 15) # cache for 15 minutes
def trending_hashtags(request):
# expensive DB aggregation
trends = Hashtag.objects.annotate(
count=Count("mentions")
).order_by("-count")[:20]
return JsonResponse({"trends": list(trends.values())})
# Class-based view
@method_decorator(cache_page(60 * 15), name="dispatch")
class TrendingView(View):
def get(self, request):
...الـ cache على مستوى الـ view بيشتغل كويس مع الـ endpoints العامة اللي أغلب الطلبات عليها قراءة (read-heavy)، والـ response فيها واحد لكل المستخدمين. لكن مبينفعش مع الـ views اللي محتاجة authentication والـ response فيها بيختلف من مستخدم للتاني — الـ cache في الحالة دي بيسرّب بيانات بين المستخدمين.
المستوى 2: cache لجزء من الـ template
لما يكون جزء بس من الصفحة هو اللي الـ render بتاعه مكلف، اعملوا cache للجزء ده بدل الـ response كله. الـ template engine في Django فيه cache tag جاهز للموضوع ده.
{% load cache %}
{# Cache this block for 10 minutes, keyed by platform name #}
{% cache 600 platform_stats platform.name %}
<div class="stats-panel">
<span>{{ platform.total_mentions }}</span>
<span>{{ platform.top_influencer }}</span>
</div>
{% endcache %}المستوى 3: الـ low-level cache API
لو محتاجين تحكم أدق — زي إنكم تعملوا cache لنتيجة function واحدة مكلفة، مش للـ view كله — استخدموا الـ low-level cache API مباشرةً. ومن تجربتي، هنا عادةً بتكون أكبر المكاسب.
from django.core.cache import cache
def get_top_influencers(platform: str, limit: int = 10) -> list:
cache_key = f"top_influencers:{platform}:{limit}"
result = cache.get(cache_key)
if result is not None:
return result
# Expensive aggregation query
result = (
Influencer.objects
.filter(platform=platform, is_active=True)
.annotate(engagement=Sum("post__engagement_score"))
.order_by("-engagement")
.values("id", "username", "engagement")[:limit]
)
result = list(result) # evaluate the queryset
cache.set(cache_key, result, timeout=60) # 60 second TTL
return resultاختاروا الـ TTL على أساس البيانات مسموح لها تبقى قديمة قد إيه، مش على أساس قيمة default. في لوحات التحكم في Twiscope، الـ aggregates اللي مبتتغيرش غير لما تقارير جديدة توصل بيتعملها cache لكل مستخدم لمدة 5–15 دقيقة؛ وأي حاجة المستخدمين متوقعين يشوفوها بتتغير فورًا يا إما مبيتعملهاش cache، يا إما بيتعملها invalidation أول ما يحصل write.
الـ cache invalidation: الجزء الصعب
الجزء السهل هو تخزين القيمة؛ القرار الأصعب هو إمتى ما ينفعش نعتمد عليها. في Django عندكم اختيارين أساسيين: expiry بمدة TTL، أو invalidation صريح لما البيانات تتغيّر.
from django.core.cache import cache
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=Influencer)
def invalidate_influencer_cache(sender, instance, **kwargs):
# Clear all cached influencer lists for this platform
pattern = f"top_influencers:{instance.platform}:*"
transaction.on_commit(
lambda: cache.delete_pattern(pattern) # requires django-redis
)
# Or use versioned cache keys — increment version on write
def get_cache_version(platform: str) -> int:
key = f"version:{platform}"
cache.add(key, 1, timeout=None)
return cache.get(key)
def bump_cache_version(platform: str):
key = f"version:{platform}"
def increment():
cache.add(key, 1, timeout=None)
cache.incr(key, delta=1)
transaction.on_commit(increment)
def versioned_cache_key(platform: str, limit: int) -> str:
version = get_cache_version(platform)
return f"top_influencers:{platform}:v{version}:{limit}"الـ invalidation المرتبط بتغيير في قاعدة البيانات يتنفّذ بعد نجاح الـ transaction، مش قبلها؛ وإلا request تاني ممكن يقرأ البيانات القديمة ويخزّنها من جديد قبل الـ commit. ولو هتستخدموا versioned keys، لازم الـ version يدخل فعلًا في الـ cache key، ويتعمله initialization قبل أول incr، ويفضل من غير expiry.
إيه اللي بلاش نعمله cache
- بيانات خاصة بكل مستخدم من غير cache keys منفصلة لكل مستخدم — كده هتسرّبوا بيانات بين المستخدمين
- بيانات بتتغير مع كل write لو معدل الـ writes عندكم عالي — الـ cache كده كده هيتعمله invalidation فورًا
- الـ queries الصغيرة والسريعة — الـ overhead بتاع البحث في الـ cache ممكن يزيد عن وقت الـ query نفسها في الـ lookups البسيطة بالـ primary key
- أي حاجة حساسة أمنيًا (الـ tokens، كلمات السر، الصلاحيات) — قرارات الـ TTL الغلط عواقبها خطيرة
مراقبة فعالية الـ cache
الـ cache اللي مش قادرين تقيسوه مش هتعرفوا إذا كان بيفيد فعلًا. تابعوا الـ hit rate والـ miss rate والـ eviction rate، واربطوهم بزمن الاستجابة وتكلفة الـ queries. مع Redis، الأرقام الأساسية بتطلع من INFO stats. مفيش hit rate مناسب لكل الأنظمة؛ الرقم المطلوب بيتحدد من تكلفة الـ miss وطبيعة البيانات.
# Redis cache stats
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
# Hit rate = hits / (hits + misses)
# Target: > 80% for hot-path cachesالـ caching framework في Django ممكن يدي نتيجة سريعة لما نفس الشغل المكلف بيتكرر، لكنه مش بديل عن تحسين الـ query أو إضافة index ناقص. ابدأوا بأغلى queries المتكررة، قيسوا النتيجة، وحددوا بوضوح قد إيه البيانات مسموح لها تبقى قديمة قبل ما توسّعوا استخدام الـ cache.