Chapter 16: Performance Optimization
16.2 Cache System
Cache Framework
Django provides multiple cache backends and strategies:
python
# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
},
'session': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2',
}
}
# Using local memory cache (development environment)
# CACHES = {
# 'default': {
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
# 'LOCATION': 'unique-snowflake',
# }
# }
# Using database cache
# python manage.py createcachetable
# CACHES = {
# 'default': {
# 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
# 'LOCATION': 'my_cache_table',
# }
# }
# Cache sessions
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
SESSION_CACHE_ALIAS = 'session'
# Cache middleware configuration
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware', # Must be first
# ... other middleware ...
'django.middleware.cache.FetchFromCacheMiddleware', # Must be last
]
CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 600 # Cache for 10 minutes
CACHE_MIDDLEWARE_KEY_PREFIX = ''Page Cache
Page-level caching can significantly improve performance:
python
# views.py
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView
# Function view page cache
@cache_page(60 * 15) # Cache for 15 minutes
def article_list(request):
articles = Article.objects.select_related('author', 'category').filter(
status='published'
).order_by('-created_at')[:20]
return render(request, 'blog/article_list.html', {'articles': articles})
# Class view page cache
@method_decorator(cache_page(60 * 15), name='dispatch')
class ArticleListView(ListView):
model = Article
template_name = 'blog/article_list.html'
context_object_name = 'articles'
paginate_by = 10
def get_queryset(self):
return Article.objects.select_related('author', 'category').filter(
status='published'
).order_by('-created_at')
# Parameter-based caching
@cache_page(60 * 15, key_prefix='article_detail')
def article_detail(request, slug):
article = get_object_or_404(
Article.objects.select_related('author', 'category').prefetch_related('tags'),
slug=slug,
status='published'
)
return render(request, 'blog/article_detail.html', {'article': article})
# Conditional page caching
from django.views.decorators.vary import vary_on_headers
@cache_page(60 * 15)
@vary_on_headers('User-Agent', 'Cookie')
def responsive_page(request):
# Page that returns different content based on User-Agent or Cookie
return render(request, 'responsive_page.html')
# Custom cache key
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.utils.cache import get_cache_key
def custom_cache_key(request):
"""Generate custom cache key"""
key = f"article_detail_{request.user.id if request.user.is_authenticated else 'anonymous'}_{request.GET.get('slug')}"
return key
def article_detail_with_custom_cache(request, slug):
cache_key = custom_cache_key(request)
article = cache.get(cache_key)
if article is None:
article = get_object_or_404(
Article.objects.select_related('author', 'category').prefetch_related('tags'),
slug=slug,
status='published'
)
# Cache for 1 hour
cache.set(cache_key, article, 60 * 60)
return render(request, 'blog/article_detail.html', {'article': article})View Cache
View-level caching provides more precise control:
python
# views.py
from django.views.decorators.cache import cache_control, never_cache
from django.utils.decorators import method_decorator
from django.core.cache import cache
# Cache control decorator
@cache_control(max_age=3600, public=True) # Cache for 1 hour, allow public cache
def article_list(request):
articles = Article.objects.select_related('author', 'category').filter(
status='published'
).order_by('-created_at')[:20]
return render(request, 'blog/article_list.html', {'articles': articles})
# Disable cache
@never_cache
def user_profile(request):
# User profile should not be cached
return render(request, 'accounts/profile.html', {
'profile': request.user.profile
})
# Class view cache control
@method_decorator(cache_control(max_age=3600), name='dispatch')
class ArticleListView(ListView):
# ... view implementation ...
# Manual cache management
class ArticleDetailView(DetailView):
model = Article
template_name = 'blog/article_detail.html'
context_object_name = 'article'
def get_object(self, queryset=None):
slug = self.kwargs['slug']
cache_key = f'article_{slug}'
# Try to get from cache
article = cache.get(cache_key)
if article is None:
# Cache miss, get from database
article = super().get_object(queryset)
# Cache for 2 hours
cache.set(cache_key, article, 60 * 60 * 2)
else:
# Update view count (even when retrieved from cache)
article.view_count += 1
article.save(update_fields=['view_count'])
return article
def dispatch(self, request, *args, **kwargs):
# Check if there's a cached version
response = cache.get(f'article_response_{self.kwargs["slug"]}')
if response is not None and not request.user.is_authenticated:
# Return cached response only for anonymous users
return response
response = super().dispatch(request, *args, **kwargs)
# Cache response for anonymous users
if not request.user.is_authenticated:
cache.set(
f'article_response_{self.kwargs["slug"]}',
response,
60 * 60 # Cache for 1 hour
)
return response
# Cache invalidation mechanism
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
@receiver(post_save, sender=Article)
def invalidate_article_cache(sender, instance, **kwargs):
"""Clear related cache when article is saved"""
cache.delete(f'article_{instance.slug}')
cache.delete_many([
'article_list',
f'user_articles_{instance.author.id}',
f'category_articles_{instance.category.id if instance.category else 0}'
])
@receiver(post_delete, sender=Article)
def invalidate_article_cache_on_delete(sender, instance, **kwargs):
"""Clear related cache when article is deleted"""
cache.delete(f'article_{instance.slug}')
cache.delete('article_list')Template Fragment Cache
Template-level caching can cache specific parts of a page:
html
<!-- templates/blog/article_list.html -->
{% load cache %}
<!DOCTYPE html>
<html>
<head>
<title>Blog Article List</title>
</head>
<body>
<!-- Cache sidebar for 1 hour -->
{% cache 3600 sidebar %}
<div class="sidebar">
<h3>Popular Articles</h3>
{% for article in popular_articles %}
<div class="popular-article">
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</div>
{% endfor %}
</div>
{% endcache %}
<!-- Cache article list for 30 minutes -->
{% cache 1800 article_list page_obj.number %}
<div class="article-list">
{% for article in articles %}
<article class="article-item">
<h2><a href="{{ article.get_absolute_url }}">{{ article.title }}</a></h2>
<p class="meta">
Author: {{ article.author.username }} |
Category: {{ article.category.name }} |
Published: {{ article.created_at|date:"Y-m-d" }}
</p>
<div class="excerpt">{{ article.excerpt|truncatewords:50 }}</div>
</article>
{% endfor %}
</div>
{% endcache %}
<!-- Use vary_on_cache to cache different versions based on user status -->
{% load cache %}
{% if user.is_authenticated %}
{% cache 1800 user_welcome user.id %}
<div class="welcome">
<h3>Welcome back, {{ user.username }}!</h3>
<p>You have {{ user.unread_notifications.count }} unread messages</p>
</div>
{% endcache %}
{% else %}
{% cache 1800 guest_welcome %}
<div class="welcome">
<h3>Welcome to our blog!</h3>
<p><a href="{% url 'accounts:login' %}">Login</a> to get more features</p>
</div>
{% endcache %}
{% endif %}
</body>
</html>python
# Use template fragment cache in views
def article_list(request):
# Prepare data for template cache
context = {
'articles': Article.objects.select_related('author', 'category').filter(
status='published'
).order_by('-created_at')[:20],
'popular_articles': cache.get('popular_articles')
}
if context['popular_articles'] is None:
context['popular_articles'] = Article.objects.filter(
status='published',
view_count__gt=1000
).order_by('-view_count')[:5]
# Cache popular articles for 1 hour
cache.set('popular_articles', context['popular_articles'], 3600)
return render(request, 'blog/article_list.html', context)
# Custom template cache tag
# templatetags/cache_extras.py
from django import template
from django.core.cache import cache
register = template.Library()
@register.simple_tag
def cached_query(queryset, cache_key, timeout=300):
"""Template tag to cache query results"""
result = cache.get(cache_key)
if result is None:
result = list(queryset)
cache.set(cache_key, result, timeout)
return result
# Use custom cache tag in template
"""
{% load cache_extras %}
{% cached_query popular_articles "popular_articles" 3600 as cached_popular %}
<div class="sidebar">
<h3>Popular Articles</h3>
{% for article in cached_popular %}
<div class="popular-article">
<a href="{{ article.get_absolute_url }}">{{ article.title }}</a>
</div>
{% endfor %}
</div>
"""Cache Best Practices
Best practices for cache usage:
python
# Cache key naming conventions
class CacheKeys:
"""Cache key naming conventions"""
@staticmethod
def article_detail(slug):
return f"article_detail:{slug}"
@staticmethod
def user_profile(user_id):
return f"user_profile:{user_id}"
@staticmethod
def category_articles(category_id, page=1):
return f"category_articles:{category_id}:page:{page}"
@staticmethod
def search_results(query, page=1):
# Clean query string to avoid illegal characters
clean_query = query.replace(' ', '_').replace(':', '_')
return f"search_results:{clean_query}:page:{page}"
# Cache version management
class CacheVersion:
"""Cache version management"""
VERSION = 'v1' # Cache version number
@classmethod
def get_key(cls, key):
return f"{cls.VERSION}:{key}"
# Usage example
def get_article_detail(slug):
cache_key = CacheVersion.get_key(CacheKeys.article_detail(slug))
article = cache.get(cache_key)
if article is None:
article = get_object_or_404(Article, slug=slug)
cache.set(cache_key, article, 3600) # Cache for 1 hour
return article
# Cache warming
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""Cache warming command"""
def handle(self, *args, **options):
# Warm popular articles
popular_articles = Article.objects.filter(
status='published'
).order_by('-view_count')[:100]
for article in popular_articles:
cache_key = CacheKeys.article_detail(article.slug)
cache.set(cache_key, article, 3600)
# Warm category pages
categories = Category.objects.all()
for category in categories:
cache_key = f"category_{category.id}_articles"
articles = Article.objects.filter(
category=category, status='published'
).order_by('-created_at')[:20]
cache.set(cache_key, list(articles), 1800)
self.stdout.write(
self.style.SUCCESS('Cache warming completed')
)
# Cache monitoring and statistics
import logging
from django.core.cache import cache
logger = logging.getLogger(__name__)
class CacheMonitor:
"""Cache monitoring"""
@staticmethod
def get_cache_stats():
"""Get cache statistics"""
try:
# Redis statistics
from django_redis import get_redis_connection
redis_conn = get_redis_connection("default")
info = redis_conn.info()
return {
'used_memory': info.get('used_memory_human'),
'connected_clients': info.get('connected_clients'),
'total_commands_processed': info.get('total_commands_processed'),
'keyspace_hits': info.get('keyspace_hits'),
'keyspace_misses': info.get('keyspace_misses'),
}
except Exception as e:
logger.error(f"Failed to get cache statistics: {e}")
return {}
@staticmethod
def cache_hit_rate():
"""Calculate cache hit rate"""
stats = CacheMonitor.get_cache_stats()
hits = stats.get('keyspace_hits', 0)
misses = stats.get('keyspace_misses', 0)
if hits + misses > 0:
return hits / (hits + misses)
return 0
# Record cache hits in views
def monitored_article_detail(request, slug):
cache_key = CacheKeys.article_detail(slug)
# Record cache query start
start_time = time.time()
article = cache.get(cache_key)
cache_time = time.time() - start_time
if article is None:
# Cache miss
logger.info(f"Cache miss: {cache_key}, Query time: {cache_time:.3f}s")
article = get_object_or_404(Article, slug=slug)
cache.set(cache_key, article, 3600)
else:
# Cache hit
logger.info(f"Cache hit: {cache_key}, Cache time: {cache_time:.3f}s")
return render(request, 'blog/article_detail.html', {'article': article})Through proper use of Django's cache system, you can significantly improve application performance, reduce database load, and enhance user experience.
Summary
Core points of Django cache system:
- ✅ Configure appropriate cache backends (Redis, Memcached, etc.)
- ✅ Implement page-level, view-level, and template fragment caching
- ✅ Design effective cache key naming conventions and version management
- ✅ Establish cache invalidation mechanisms to ensure data consistency
- ✅ Monitor cache performance and optimize
Caching is an important means to improve Web application performance.
Next Article
We will learn about production environment deployment solutions.
17.1 Production Environment Configuration →