Cache backends, per-view cache, template fragments, low-level API, invalidation
Кэширование — ключевой инструмент для ускорения Django приложений. Redis — лучший выбор для production благодаря скорости и функциональности.
💡 Правило: Кэшируйте то, что дорого вычислять, но часто запрашивается. Не кэшируйте персональные данные без необходимости. Всегда планируйте инвалидацию.
# Установка Redis (Ubuntu)
sudo apt-get install redis-server
# Проверка работы
redis-cli ping # Должен вернуть PONG
# Установка Python клиента
pip install redis# settings.py
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1', # Database 1
'TIMEOUT': 300, # 5 минут по умолчанию
'OPTIONS': {
'password': 'your_redis_password', # Если требуется
'db': 1,
}
},
'alternate': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2',
'TIMEOUT': 600,
}
}# settings.py
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = Truefrom django.core.cache import cache
# Установка
cache.set('my_key', 'hello world!', 300) # 300 секунд
# Получение
value = cache.get('my_key')
value = cache.get('my_key', 'default_value') # Если ключ не найден
# Удаление
cache.delete('my_key')
# Очистка всего кэша (ОПАСНО!)
cache.clear()
# Атомарные операции
cache.add('another_key', 'value', 300) # Только если ключа нет
cache.set_many({'a': 1, 'b': 2}, 300)
result = cache.get_many(['a', 'b', 'c']) # {'a': 1, 'b': 2}
cache.delete_many(['a', 'b'])
# Get or set
value = cache.get_or_set('key', 'default', 300)
# Increment/Decrement (только для чисел)
cache.incr('counter') # +1
cache.incr('counter', 10) # +10
cache.decr('counter') # -1from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
# Для FBV
@cache_page(60 * 15) # 15 минут
def my_view(request):
# ...
return response
# С ключом
@cache_page(60 * 15, cache='alternate')
def cached_view(request):
pass
# С префиксом
@cache_page(60 * 15, key_prefix='myapp')
def prefixed_view(request):
pass
# Для CBV
@method_decorator(cache_page(60 * 15), name='dispatch')
class MyView(View):
def get(self, request):
pass<!-- Загрузите cache тег -->
{% load cache %}
<!-- Базовое использование -->
{% cache 500 sidebar %}
<!-- Кэшируется на 500 секунд -->
{% include 'sidebar.html' %}
{% endcache %}
<!-- С переменными в ключе -->
{% cache 500 sidebar user.id %}
<!-- Разный кэш для каждого пользователя -->
{% include 'sidebar.html' %}
{% endcache %}
<!-- С несколькими переменными -->
{% cache 500 sidebar user.id page_type %}
<!-- ... -->
{% endcache %}
<!-- С версией -->
{% cache 500 sidebar user.id version=2 %}
<!-- Версия позволяет инвалидировать весь кэш изменением version -->
{% endcache %}
<!-- С named cache -->
{% cache 500 sidebar user.id using='alternate' %}
<!-- Использует 'alternate' кэш -->
{% endcache %}from django.core.cache import cache
def get_expensive_data(obj_id):
"""Паттерн cache-aside."""
key = f'expensive_data:{obj_id}'
# Попытка получить из кэша
data = cache.get(key)
if data is None:
# Cache miss — загрузка из БД
data = ExpensiveModel.objects.filter(id=obj_id).select_related().first()
# Сохранение в кэш
cache.set(key, data, 60 * 30) # 30 минут
return datadef update_data(obj_id, new_data):
"""Обновление с синхронной записью в кэш."""
key = f'expensive_data:{obj_id}'
# Обновление в БД
obj = ExpensiveModel.objects.get(id=obj_id)
obj.data = new_data
obj.save()
# Обновление в кэше
cache.set(key, obj, 60 * 30)
return objfrom django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
@receiver(post_save, sender=ExpensiveModel)
@receiver(post_delete, sender=ExpensiveModel)
def invalidate_cache(sender, instance, **kwargs):
"""Инвалидация кэша при изменении."""
key = f'expensive_data:{instance.id}'
cache.delete(key)
# Также инвалидируем списки
cache.delete('expensive_data:list')# settings.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': [
'redis://node1:6379/0',
'redis://node2:6379/0',
'redis://node3:6379/0',
],
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.ShardClient',
}
}
}pip install django-redis# settings.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'PASSWORD': 'secret',
'SOCKET_CONNECT_TIMEOUT': 5,
'SOCKET_TIMEOUT': 5,
'CONNECTION_POOL_KWARGS': {
'max_connections': 50,
'retry_on_timeout': True,
},
'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',
}
}
}# Использование расширенных функций
from django_redis import get_redis_connection
# Прямое подключение к Redis
redis_conn = get_redis_connection('default')
# Redis команды
redis_conn.hset('myhash', 'key', 'value')
redis_conn.hget('myhash', 'key')
# Pipeline для пакетных операций
pipe = redis_conn.pipeline()
pipe.set('a', 1)
pipe.set('b', 2)
pipe.execute()from django_redis import get_redis_connection
def get_cache_stats():
"""Получить статистику кэша."""
redis_conn = get_redis_connection('default')
# Информация о Redis
info = redis_conn.info('stats')
# Использование памяти
memory = redis_conn.info('memory')
# Количество ключей
keys_count = redis_conn.dbsize()
return {
'hits': info.get('keyspace_hits', 0),
'misses': info.get('keyspace_misses', 0),
'memory_used': memory.get('used_memory_human', 'N/A'),
'keys_count': keys_count,
}import logging
from django.core.cache import cache
logger = logging.getLogger('cache')
class LoggingCache:
"""Кэш с логированием miss."""
def __init__(self):
self.cache = cache
def get(self, key, default=None):
value = self.cache.get(key, default)
if value is None:
logger.warning(f'Cache miss: {key}')
return value
def set(self, key, value, timeout=None):
logger.debug(f'Cache set: {key}')
self.cache.set(key, value, timeout)
def delete(self, key):
logger.info(f'Cache delete: {key}')
self.cache.delete(key)
# Использование
cache = LoggingCache()from django.core.cache import cache
import threading
_locks = {}
def get_with_lock(key, func, timeout=300):
"""Избегаем stampede с блокировками."""
value = cache.get(key)
if value is not None:
return value
# Блокировка для перестройки кэша
lock_key = f'{key}:lock'
if not cache.add(lock_key, True, 10): # 10 секунд lock
# Кто-то уже перестраивает — ждём
import time
time.sleep(0.1)
return cache.get(key)
try:
value = func()
cache.set(key, value, timeout)
return value
finally:
cache.delete(lock_key)
# Использование
def get_expensive_data(obj_id):
return get_with_lock(
f'data:{obj_id}',
lambda: ExpensiveModel.objects.get(id=obj_id),
timeout=300
)from django.core.cache import cache
import time
def dogpile_get(key, create_function, timeout=300):
"""Dogpile lock паттерн."""
val = cache.get(key)
if val is not None:
return val
lock_key = f'{key}:lock'
if cache.add(lock_key, True, timeout=timeout):
try:
val = create_function()
cache.set(key, val, timeout=timeout)
return val
finally:
cache.delete(lock_key)
else:
# Ждём пока другой поток перестроит
time.sleep(0.1)
return cache.get(key)# blog/cache.py
from django.core.cache import cache
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import Post, Category
class BlogCache:
"""Управление кэшем для блога."""
TIMEOUT = 60 * 30 # 30 минут
@staticmethod
def get_post(post_id):
"""Получить пост из кэша."""
key = f'post:{post_id}'
return cache.get(key)
@staticmethod
def set_post(post):
"""Сохранить пост в кэш."""
key = f'post:{post.id}'
cache.set(key, post, BlogCache.TIMEOUT)
@staticmethod
def delete_post(post_id):
"""Удалить пост из кэша."""
key = f'post:{post_id}'
cache.delete(key)
@staticmethod
def get_category_posts(category_slug, page=1):
"""Получить посты категории."""
key = f'category:{category_slug}:page:{page}'
return cache.get(key)
@staticmethod
def set_category_posts(category_slug, posts, page=1):
"""Сохранить посты категории в кэш."""
key = f'category:{category_slug}:page:{page}'
cache.set(key, posts, BlogCache.TIMEOUT)
@staticmethod
def invalidate_category(category_slug):
"""Инвалидировать кэш категории."""
pattern = f'category:{category_slug}:*'
# Для Redis можно использовать keys (не рекомендуется в production)
# Или хранить список ключей
cache.delete_pattern(pattern) # django-redis
@staticmethod
def get_trending_posts():
"""Получить трендовые посты."""
return cache.get('trending:posts')
@staticmethod
def set_trending_posts(posts):
"""Сохранить трендовые посты."""
cache.set('trending:Posts', posts, 60 * 15) # 15 минут
# Сигналы для инвалидации
@receiver(post_save, sender=Post)
@receiver(post_delete, sender=Post)
def invalidate_post_cache(sender, instance, **kwargs):
"""Инвалидировать кэш поста."""
BlogCache.delete_post(instance.id)
# Инвалидировать кэш категории
if hasattr(instance, 'category'):
BlogCache.invalidate_category(instance.category.slug)
# Инвалидировать тренды
cache.delete('trending:Posts')
# blog/views.py
from django.shortcuts import render, get_object_or_404
from django.core.cache import cache
from .models import Post, Category
from .cache import BlogCache
def post_detail(request, pk):
"""Детальный просмотр поста с кэшированием."""
# Попытка кэша
post = BlogCache.get_post(pk)
if post is None:
# Cache miss
post = get_object_or_404(Post.objects.select_related('author'), pk=pk)
BlogCache.set_post(post)
return render(request, 'blog/post_detail.html', {'post': post})
def category_posts(request, slug):
"""Посты категории с кэшированием."""
page = request.GET.get('page', 1)
posts = BlogCache.get_category_posts(slug, page)
if posts is None:
category = get_object_or_404(Category, slug=slug)
posts = category.posts.select_related('author').all()
BlogCache.set_category_posts(slug, posts, page)
return render(request, 'blog/category.html', {'category': category, 'posts': posts})
def trending_posts(request):
"""Трендовые посты."""
posts = BlogCache.get_trending_posts()
if posts is None:
posts = Post.objects.annotate(
engagement=Count('comments') + Count('likes') * 2
).order_by('-engagement', '-views')[:10]
BlogCache.set_trending_posts(posts)
return render(request, 'blog/trending.html', {'posts': posts})Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.