Кеширование и Middleware для получения статуса пользователей
Зачем кеш
Каждая страница сайта статей делает запросы к базе: статьи, категории (дерево!), теги, рейтинги. Большинство данных меняется редко, а запрашивается постоянно. Кеш хранит готовый результат и отдаёт его без повторных вычислений.
Бэкенды
| Бэкенд | Когда использовать |
|---|---|
LocMemCache |
По умолчанию, разработка. Свой у каждого процесса |
FileBasedCache |
Простой хостинг без Redis |
DatabaseCache |
Кеш в таблице БД |
RedisCache |
Продакшен: быстрый, общий для всех воркеров Gunicorn |
DummyCache |
Отключить кеш (тесты, отладка) |
sudo apt install redis-server
pip install redis
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
Уровни кеширования
Целая страница
from django.views.decorators.cache import cache_page
@cache_page(60 * 5)
def article_list(request):
...
Для CBV — в urls.py:
path("", cache_page(60 * 5)(ArticleListView.as_view()), name="home"),
Осторожно: страница кешируется целиком, вместе с шапкой «Привет, Анна». Ключ кеша учитывает URL и заголовки из Vary — Django добавляет Vary: Cookie, если страница использует сессию, но проще кешировать целиком только публичные страницы.
Фрагмент шаблона
Чаще всего выгоднее кешировать тяжёлые куски — меню категорий, облако тегов:
{% load cache %}
{% cache 600 sidebar %}
{% popular_categories %}
{% popular_tags %}
{% endcache %}
Дополнительные аргументы делают ключ уникальным, например для каждого пользователя: {% cache 600 profile_block request.user.id %}.
Низкоуровневый API
from django.core.cache import cache
cache.set("key", value, timeout=300)
cache.get("key") # None, если нет или истёк
cache.get_or_set("categories", lambda: list(Category.objects.all()), 600)
cache.delete("categories")
Сброс при изменении данных — например, в сигнале:
@receiver([post_save, post_delete], sender=Category)
def clear_categories_cache(sender, **kwargs):
cache.delete("categories")
Сбросить фрагментный кеш помогает make_template_fragment_key:
from django.core.cache.utils import make_template_fragment_key
cache.delete(make_template_fragment_key("sidebar"))
Middleware
Middleware — слой, через который проходит каждый запрос до представления и каждый ответ после него. Вы уже пользуетесь ими: сессии, CSRF, аутентификация, Debug Toolbar.
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response # один раз при запуске сервера
def __call__(self, request):
# код до представления
response = self.get_response(request)
# код после представления
return response
Порядок в MIDDLEWARE важен: запрос идёт сверху вниз, ответ — снизу вверх.
Статус «онлайн»
Хотим показывать в профиле: «В сети» или «Был(а) 2 часа назад». Записывать время каждого запроса в базу дорого, а в кеш — дёшево.
modules/system/middleware.py:
from django.core.cache import cache
from django.utils import timezone
class ActiveUserMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
cache.set(f"last-seen-{request.user.id}", timezone.now(), 300)
return self.get_response(request)
MIDDLEWARE = [
...
"django.contrib.auth.middleware.AuthenticationMiddleware",
"modules.system.middleware.ActiveUserMiddleware",
...
]
После AuthenticationMiddleware — иначе request.user ещё не существует.
Метод в Profile:
from datetime import timedelta
from django.core.cache import cache
from django.utils import timezone
class Profile(models.Model):
...
def is_online(self):
last_seen = cache.get(f"last-seen-{self.user.id}")
if last_seen is not None and timezone.now() < last_seen + timedelta(seconds=300):
return True
return False
В шаблоне профиля:
{% if profile.is_online %}
<span class="badge bg-success">В сети</span>
{% else %}
<span class="badge bg-secondary">Не в сети</span>
{% if profile.user.last_login %}<small>Последний вход: {{ profile.user.last_login|timesince }} назад</small>{% endif %}
{% endif %}
Запись истекает через 5 минут без запросов — пользователь становится «не в сети». С LocMemCache статус будет «прыгать» между воркерами Gunicorn: используйте Redis.
Итог
- Кеш хранит готовые результаты: целые страницы (
cache_page), фрагменты ({% cache %}), данные (cache.get/set). - В продакшене — Redis, общий для всех процессов; сбрасывайте кеш при изменении данных.
- Middleware — класс с
__init__(get_response)и__call__(request), порядок вMIDDLEWAREважен. - Статус «онлайн» — метка времени в кеше, которую ставит middleware.
Практика решено 0 из 6
Настройте кеш на Redis: в CACHES["default"] укажите "BACKEND": "django.core.cache.backends.redis.RedisCache"
и "LOCATION": "redis://127.0.0.1:6379".
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
Закешируйте боковое меню категорий на 10 минут фрагментным кешем:
{% load cache %}, затем {% cache 600 sidebar %} … {% endcache %} вокруг {% popular_categories %}.
{% load cache %}
{% cache 600 sidebar %}
{% popular_categories %}
{% endcache %}
Напишите middleware ActiveUserMiddleware в виде класса: __init__(self, get_response) сохраняет
self.get_response; __call__(self, request) для вошедшего пользователя
(request.user.is_authenticated) записывает в кеш cache.set(f"last-seen-{request.user.id}", timezone.now(), 300)
и возвращает self.get_response(request).
from django.core.cache import cache
from django.utils import timezone
class ActiveUserMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
cache.set(f"last-seen-{request.user.id}", timezone.now(), 300)
return self.get_response(request)
Где в MIDDLEWARE должно стоять ActiveUserMiddleware?
Закешируйте представление-функцию article_list на 5 минут декоратором @cache_page(60 * 5)
из django.views.decorators.cache.
from django.views.decorators.cache import cache_page
@cache_page(60 * 5)
def article_list(request):
...
Когда кеш нужно сбросить или он может навредить?
Войдите, чтобы отмечать пройденные уроки.