Кеширование и Middleware для получения статуса пользователей

⏱ 35 мин · 🧩 заданий: 6

Зачем кеш

Каждая страница сайта статей делает запросы к базе: статьи, категории (дерево!), теги, рейтинги. Большинство данных меняется редко, а запрашивается постоянно. Кеш хранит готовый результат и отдаёт его без повторных вычислений.

Бэкенды

Бэкенд Когда использовать
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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Настройте кеш на Redis: в CACHES["default"] укажите "BACKEND": "django.core.cache.backends.redis.RedisCache" и "LOCATION": "redis://127.0.0.1:6379".

Задание 2 Написать код

Закешируйте боковое меню категорий на 10 минут фрагментным кешем: {% load cache %}, затем {% cache 600 sidebar %} … {% endcache %} вокруг {% popular_categories %}.

Задание 3 Написать код

Напишите 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).

Задание 4 Один вариант ответа

Где в MIDDLEWARE должно стоять ActiveUserMiddleware?

Задание 5 Написать код

Закешируйте представление-функцию article_list на 5 минут декоратором @cache_page(60 * 5) из django.views.decorators.cache.

Задание 6 Несколько вариантов

Когда кеш нужно сбросить или он может навредить?

Войдите, чтобы отмечать пройденные уроки.