Skip to content

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:

  1. ✅ Configure appropriate cache backends (Redis, Memcached, etc.)
  2. ✅ Implement page-level, view-level, and template fragment caching
  3. ✅ Design effective cache key naming conventions and version management
  4. ✅ Establish cache invalidation mechanisms to ensure data consistency
  5. ✅ 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 →

Directory

Return to Course Directory

Released under the [BY-NC-ND License](https://creativecommons.org/licenses/by-nc-nd/4.0/deed.en).