Skip to content

Latest commit

 

History

History
538 lines (352 loc) · 26.2 KB

File metadata and controls

538 lines (352 loc) · 26.2 KB

Урок 30. Кэширование с Redis: установка, настройка, кэширование страниц

Зачем нужно кэширование

Каждый раз, когда пользователь открывает страницу каталога фильмов, Django выполняет одну и ту же последовательность: получает запрос, делает SQL-запрос к базе, рендерит шаблон, возвращает HTML. Для статичных или редко меняющихся страниц это лишняя работа — результат будет таким же, как и секунду назад.

Кэширование решает это просто: сохранить готовый результат и при следующем запросе отдать его напрямую, минуя базу данных и рендеринг. Для каталога фильмов, который меняется редко, это даёт ощутимый прирост скорости.

Django поддерживает несколько бэкендов кэширования: файловая система, память процесса, база данных и Redis. Redis — стандартный выбор для продакшна: он работает в оперативной памяти, очень быстр и умеет управлять временем жизни ключей.


Запуск Redis с помощью Docker

Для работы с кэшированием нам понадобится Redis — отдельный сервер, который будет хранить данные нашего кэша. Его можно установить непосредственно в операционную систему, однако существует и другой подход — запускать Redis внутри Docker-контейнера.

Что такое Docker?

Docker — это платформа, которая позволяет запускать приложения и сервисы в изолированных контейнерах.

Контейнер можно представить как отдельную среду, внутри которой находится всё необходимое для работы конкретного приложения или сервиса.

Например, вместо того чтобы устанавливать Redis непосредственно в операционную систему, мы можем запустить Redis внутри Docker-контейнера.

Для нашего проекта это удобно по нескольким причинам:

  • не нужно отдельно устанавливать Redis в операционную систему;
  • не нужно настраивать Redis вручную;
  • способ запуска практически одинаковый на разных операционных системах;
  • Redis можно легко остановить, запустить или удалить;
  • установленный в контейнере Redis не засоряет операционную систему дополнительными пакетами и настройками.

В дальнейшем Docker также позволит нам запускать в контейнерах другие компоненты проекта: например, PostgreSQL, Redis, Celery и само Django-приложение.

В этом уроке мы не будем подробно изучать Docker. Наша задача — использовать его как удобный инструмент для запуска Redis.

Для начала необходимо установить Docker.

macOS и Windows

На macOS и Windows используется приложение Docker Desktop.

Скачайте и установите Docker Desktop с официального сайта Docker:

https://www.docker.com/products/docker-desktop/

После установки запустите Docker Desktop.

Docker должен быть запущен во время работы с Redis.

Linux

На Linux обычно устанавливается Docker Engine.

Инструкции по установке зависят от конкретного дистрибутива Linux. Для Ubuntu можно воспользоваться официальной инструкцией Docker.

После установки необходимо убедиться, что Docker доступен в терминале.

Проверим установленную версию:

docker --version

Если Docker установлен правильно, терминал выведет информацию о его версии.

Запуск Redis внутри Docker

Теперь запустим Redis в Docker-контейнере и выполним команду:

docker run -d --name redis -p 6379:6379 redis

Команда docker run создаёт и запускает новый контейнер на основе указанного Docker-образа.


Параметр -d запускает контейнер в фоновом режиме. После выполнения команды Redis продолжит работать, а мы сразу получим обратно доступ к терминалу.


Параметр --name задаёт имя контейнера. В нашем случае контейнер будет называться redis. Благодаря этому мы сможем обращаться к контейнеру по имени:

docker stop redis
docker start redis

Параметр -p 6379:6379 позволяет подключаться к Redis из нашей операционной системы.

Первый порт 6379 — это порт нашего компьютера.

Второй порт 6379 — это порт Redis внутри Docker-контейнера.

Таким образом, Redis внутри контейнера становится доступен на нашем компьютере по адресу localhost:6379

Порт 6379 является стандартным портом Redis.


Последнее значение redis — имя Docker-образа, на основе которого будет создан контейнер. В данном случае используется официальный образ Redis.

Проверка запущенного контейнера

После запуска Redis можно проверить список работающих Docker-контейнеров:

docker ps

В списке должен появиться контейнер с именем:

redis

Если контейнер присутствует в списке, значит Redis запущен.


Проверка Redis

Теперь проверим, что Redis действительно работает и принимает подключения. Для этого выполним команду:

docker exec -it redis redis-cli ping

Команда docker exec позволяет выполнить команду внутри уже запущенного контейнера. В нашем случае мы запускаем Redis CLI и отправляем Redis команду PING. Если всё работает правильно, получим ответ:

PONG

Это означает, что Redis успешно запущен и готов принимать запросы.


Установка Python-клиента

pip install django-redis

django-redis — это бэкенд кэша для Django, который использует redis-py под капотом и интегрируется с Django Cache Framework.


Настройка Django

Подключение Redis как бэкенда кэша

# filmsite/settings.py

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        },
        'TIMEOUT': 300,  # время жизни ключей по умолчанию — 5 минут
    }
}

Разберём LOCATION: redis://127.0.0.1:6379/1 — это адрес Redis-сервера с номером базы данных (/1). Redis поддерживает 16 баз данных (0–15) в рамках одного сервера. Используем базу 1, оставляя 0 для других возможных задач (например, Celery в будущем).

Проверка подключения

python manage.py shell
from django.core.cache import cache

cache.set('test_key', 'hello from redis', timeout=60)
print(cache.get('test_key'))  # 'hello from redis'

cache.delete('test_key')
print(cache.get('test_key'))  # None

Если cache.get() вернул значение — Redis подключён и работает.


Уровни кэширования в Django

Django предоставляет несколько уровней, от грубого к тонкому:

Уровень Что кэшируется Инструмент
Весь сайт Каждый ответ Middleware
Отдельные представления Ответ конкретного view cache_page
Фрагменты шаблона Часть HTML {% cache %}
Произвольные данные QuerySet, вычисления cache.set/get

Разберём каждый.


Кэширование отдельных представлений

Декоратор cache_page для FBV

# films/views.py
from django.views.decorators.cache import cache_page


@cache_page(60 * 15)  # 15 минут
def film_list_fbv(request):
    films = Film.objects.select_related('director').prefetch_related('genres')
    return render(request, 'films/film_list.html', {'films': films})

cache_page(секунды) — первый запрос выполняется как обычно и результат сохраняется в Redis. Все последующие запросы к тому же URL получают готовый HTML из кэша без обращения к базе данных.

Кэширование CBV через urls.py

Для CBV декоратор cache_page применяется в urls.py, а не к классу:

# films/urls.py
from django.views.decorators.cache import cache_page

urlpatterns = [
    path('', IndexView.as_view(), name='index'),
    path('films/', cache_page(60 * 15)(FilmListView.as_view()), name='film_list'),
    path('stats/', cache_page(60 * 60)(CatalogStatsView.as_view()), name='catalog_stats'),
    # остальные маршруты — без кэша (детальные страницы, формы)
]

Страница статистики кэшируется на час — она меняется редко. Список фильмов — на 15 минут. Детальные страницы фильмов, страницы с формами и страница профиля — не кэшируются: первые могут меняться, вторые вообще не должны кэшироваться (персональные данные пользователя).


Кэширование фрагментов шаблона

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

<!-- templates/base.html -->
{% load cache %}

{% cache 600 genre_sidebar %}
    <aside>
        <h3>Жанры</h3>
        <ul>
        {% for genre in genres %}
            <li>{{ genre.name }}</li>
        {% endfor %}
        </ul>
    </aside>
{% endcache %}

{% cache 600 genre_sidebar %} — первый аргумент это время жизни в секундах (10 минут), второй — уникальное имя ключа кэша. Django добавит этот фрагмент в Redis и при следующем рендере вернёт его напрямую, не выполняя цикл по жанрам.

Кэш-фрагмент с переменным ключом

Если фрагмент зависит от параметра (например, разный для каждого жанра), добавляем его к ключу:

{% cache 300 film_card film.slug %}
    <div class="film-card">
        <h3>{{ film.title }}</h3>
        <p>{{ film.description|truncatechars:80 }}</p>
    </div>
{% endcache %}

Теперь каждый фильм кэшируется отдельно по своему slug.


Ручное кэширование через cache API

Самый гибкий уровень — сохранять и получать произвольные данные вручную. Это полезно для тяжёлых QuerySet'ов, которые используются в нескольких местах.

# films/views.py
from django.core.cache import cache


def catalog_stats(request):
    # Пробуем получить из кэша
    stats = cache.get('catalog_stats')

    if stats is None:
        # Кэша нет — считаем и сохраняем
        stats = {
            'overall': Film.objects.aggregate(
                total=Count('id'),
                avg_rating=Avg('rating'),
                max_rating=Max('rating'),
            ),
            'top_directors': list(
                Director.objects.annotate(film_count=Count('films'))
                .filter(film_count__gt=0)
                .order_by('-film_count')[:5]
            ),
            'films_by_year': list(
                Film.objects.values('year')
                .annotate(count=Count('id'))
                .order_by('-year')
            ),
        }
        cache.set('catalog_stats', stats, timeout=60 * 60)  # 1 час

    return render(request, 'films/catalog_stats.html', stats)

Паттерн «получить из кэша — если нет, вычислить и сохранить» называется cache-aside (или lazy caching). Это самый распространённый подход: данные загружаются в кэш не заранее, а при первом запросе.

Обрати внимание: list() вокруг QuerySet'ов обязателен при кэшировании. QuerySet — это ленивый объект, который выполняет SQL только при обращении к данным. Если положить в кэш сам QuerySet без list(), Redis сохранит описание запроса, а не результат.


Инвалидация кэша

Кэш не вечен — данные меняются, и закэшированные страницы устаревают. Есть два способа обновить кэш:

Автоматически — через TIMEOUT. Когда время жизни истекает, следующий запрос пересчитает данные и обновит кэш.

Вручную — при изменении данных явно удаляем нужный ключ:

# films/views.py
from django.core.cache import cache


class FilmUpdateView(LoginRequiredMixin, PermissionRequiredMixin, FilmEditMixin, UpdateView):
    permission_required = 'films.change_film'
    raise_exception = True

    def form_valid(self, form):
        response = super().form_valid(form)
        # Инвалидируем кэш статистики при изменении фильма
        cache.delete('catalog_stats')
        return response

    def get_success_url(self):
        return self.object.get_absolute_url()

Для cache_page ключ формируется автоматически на основе URL — инвалидировать его сложнее. Один из способов — использовать cache.clear() (сбрасывает весь кэш) или make_template_fragment_key для фрагментов:

from django.utils.cache import make_template_fragment_key

# Инвалидируем кэш боковой панели жанров
key = make_template_fragment_key('genre_sidebar')
cache.delete(key)

Как проверить, что кэширование действительно работает

После настройки кэширования важно убедиться, что Django действительно использует Redis, а не выполняет код представления при каждом запросе. Проверить это можно несколькими способами.

Способ 1. Проверяем содержимое Redis

Самый прямой способ убедиться, что Django записывает данные в Redis, — посмотреть содержимое Redis непосредственно через Redis CLI.

Подключимся к Redis:

docker exec -it redis redis-cli

Далее подключаемся к первой базе данных redis, потому что именно ее указали в настройках проекта (redis://127.0.0.1:6379/1):

SELECT 1

Теперь выполним команду:

KEYS *

Она покажет ключи, которые сейчас существуют в Redis.

Например:

1) ":1:views.decorators.cache.cache_page..."
2) ":1:views.decorators.cache.cache_header..."

Точные имена ключей могут отличаться в зависимости от настроек и версии Django.

Если мы открыли страницу, которая использует cache_page, и после этого в Redis появились ключи, связанные с кэшем страницы, это подтверждает, что Django действительно сохраняет результат в Redis.

Чтобы выйти из Redis CLI:

exit

Способ 2. Проверяем SQL-запросы через Django Debug Toolbar

Ещё один удобный способ — посмотреть, выполняется ли запрос к базе данных при повторном открытии страницы.

Открываем страницу первый раз. При первом запросе Django:

  1. получает HTTP-запрос;
  2. проверяет наличие страницы в кэше;
  3. не находит её;
  4. выполняет код представления;
  5. делает запросы к базе данных;
  6. рендерит HTML;
  7. сохраняет готовый ответ в Redis;
  8. возвращает HTML пользователю.

При повторном запросе Django:

  1. получает HTTP-запрос;
  2. находит готовый ответ в Redis;
  3. возвращает его пользователю.

Код представления при этом повторно не выполняется. Поэтому при использовании Django Debug Toolbar можно наблюдать следующую картину:

Первый запрос:
SQL queries: несколько запросов

Повторный запрос:
SQL queries: 0

Если при первом открытии страницы выполняются SQL-запросы, а при последующих запросах их количество становится равным нулю, это является хорошим признаком того, что cache_page работает.


На практике наиболее наглядная проверка выглядит так:

  1. Открыть страницу первый раз.
  2. Убедиться через Django Debug Toolbar, что выполняются SQL-запросы.
  3. Обновить страницу.
  4. Убедиться, что повторных SQL-запросов нет.
  5. Проверить наличие ключей в Redis через redis-cli.

Если все эти проверки дают ожидаемый результат, можно с высокой уверенностью утверждать, что кэширование работает.

Что кэшировать, а что нет

Кэшировать Не кэшировать
Список фильмов (меняется редко) Страница профиля пользователя
Страница статистики Формы (CSRF-токен уникален)
Боковая панель жанров Персонализированные данные
Детальная страница фильма Страницы авторизации

Главное правило: персональные данные не кэшируются на уровне страницы. Если cache_page применить к странице профиля — первый пользователь, открывший её, закэширует свои данные, и второй пользователь увидит чужой профиль. Фрагментный кэш с переменным ключом ({% cache timeout key user.id %}) — безопасный способ кэшировать части персональных страниц.


Подводные камни

QuerySet в кэше без list()

# Кэшируется описание запроса, а не данные
cache.set('films', Film.objects.all())

# Кэшируется список объектов
cache.set('films', list(Film.objects.all()))

cache_page и авторизованные пользователи

cache_page не учитывает статус авторизации — один ответ для всех. Если страница содержит {% if user.is_authenticated %}, кэшированный HTML первого пользователя (например, авторизованного) покажет другому пользователю (например, анонимному) кнопки, которых он не должен видеть.

Решение: кэшировать только страницы, которые одинаковы для всех пользователей. Для страниц с персонализацией — фрагментный кэш.

Redis недоступен — сайт падает

По умолчанию если Redis недоступен, Django выбросит исключение при обращении к кэшу. Для продакшна стоит добавить IGNORE_EXCEPTIONS:

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'IGNORE_EXCEPTIONS': True,  # при недоступности Redis — работаем без кэша
        },
        'TIMEOUT': 300,
    }
}

С IGNORE_EXCEPTIONS = True при недоступном Redis сайт продолжит работать — просто медленнее, без кэша.


Вопросы для проверки

  1. Чем Redis лучше файлового или in-memory кэша Django для продакшн-проекта?
  2. Почему нельзя положить QuerySet в кэш без явного преобразования в список?
  3. В чём опасность применения cache_page к страницам с персонализированным контентом?
  4. Что такое паттерн cache-aside и как он реализован в нашем представлении catalog_stats?
  5. Зачем нужна инвалидация кэша и как реализовать её при изменении фильма?

Практическая задача

Тип: расширь проект

Часть 1. Установи Redis и django-redis. Настрой CACHES в settings.py с IGNORE_EXCEPTIONS = True. Проверь подключение через shell: cache.set, cache.get, cache.delete.

Часть 2. Добавь cache_page к двум представлениям в urls.py: список фильмов (15 минут) и страница статистики (1 час). Убедись, что страницы профиля, формы и детальные страницы — без кэша.

Часть 3. В CatalogStatsView переведи логику на ручное кэширование через cache API (паттерн cache-aside). При обновлении фильма через FilmUpdateView инвалидируй ключ catalog_stats.

Часть 4. В шаблоне base.html закэшируй боковую панель с последними фильмами через {% cache 600 latest_films_sidebar %}. Убедись, что виджет {% latest_films 3 %} из урока 7 находится внутри этого блока.


Предыдущий урок | Следующий урок