Каждый раз, когда пользователь открывает страницу каталога фильмов, Django выполняет одну и ту же последовательность: получает запрос, делает SQL-запрос к базе, рендерит шаблон, возвращает HTML. Для статичных или редко меняющихся страниц это лишняя работа — результат будет таким же, как и секунду назад.
Кэширование решает это просто: сохранить готовый результат и при следующем запросе отдать его напрямую, минуя базу данных и рендеринг. Для каталога фильмов, который меняется редко, это даёт ощутимый прирост скорости.
Django поддерживает несколько бэкендов кэширования: файловая система, память процесса, база данных и Redis. Redis — стандартный выбор для продакшна: он работает в оперативной памяти, очень быстр и умеет управлять временем жизни ключей.
Для работы с кэшированием нам понадобится Redis — отдельный сервер, который будет хранить данные нашего кэша. Его можно установить непосредственно в операционную систему, однако существует и другой подход — запускать Redis внутри Docker-контейнера.
Docker — это платформа, которая позволяет запускать приложения и сервисы в изолированных контейнерах.
Контейнер можно представить как отдельную среду, внутри которой находится всё необходимое для работы конкретного приложения или сервиса.
Например, вместо того чтобы устанавливать Redis непосредственно в операционную систему, мы можем запустить Redis внутри Docker-контейнера.
Для нашего проекта это удобно по нескольким причинам:
- не нужно отдельно устанавливать Redis в операционную систему;
- не нужно настраивать Redis вручную;
- способ запуска практически одинаковый на разных операционных системах;
- Redis можно легко остановить, запустить или удалить;
- установленный в контейнере Redis не засоряет операционную систему дополнительными пакетами и настройками.
В дальнейшем Docker также позволит нам запускать в контейнерах другие компоненты проекта: например, PostgreSQL, Redis, Celery и само Django-приложение.
В этом уроке мы не будем подробно изучать Docker. Наша задача — использовать его как удобный инструмент для запуска Redis.
Для начала необходимо установить Docker.
На macOS и Windows используется приложение Docker Desktop.
Скачайте и установите Docker Desktop с официального сайта Docker:
https://www.docker.com/products/docker-desktop/
После установки запустите Docker Desktop.
Docker должен быть запущен во время работы с Redis.
На Linux обычно устанавливается Docker Engine.
Инструкции по установке зависят от конкретного дистрибутива Linux. Для Ubuntu можно воспользоваться официальной инструкцией Docker.
После установки необходимо убедиться, что Docker доступен в терминале.
docker --versionЕсли Docker установлен правильно, терминал выведет информацию о его версии.
Теперь запустим Redis в Docker-контейнере и выполним команду:
docker run -d --name redis -p 6379:6379 redisКоманда docker run создаёт и запускает новый контейнер на основе указанного Docker-образа.
Параметр -d запускает контейнер в фоновом режиме. После выполнения команды Redis продолжит работать, а мы сразу получим обратно доступ к терминалу.
Параметр --name задаёт имя контейнера. В нашем случае контейнер будет называться redis. Благодаря этому мы сможем обращаться к контейнеру по имени:
docker stop redisdocker 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 действительно работает и принимает подключения. Для этого выполним команду:
docker exec -it redis redis-cli pingКоманда docker exec позволяет выполнить команду внутри уже запущенного контейнера. В нашем случае мы запускаем Redis CLI и отправляем Redis команду PING. Если всё работает правильно, получим ответ:
PONG
Это означает, что Redis успешно запущен и готов принимать запросы.
pip install django-redisdjango-redis — это бэкенд кэша для Django, который использует redis-py под капотом и интегрируется с Django Cache Framework.
# 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 shellfrom 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 предоставляет несколько уровней, от грубого к тонкому:
| Уровень | Что кэшируется | Инструмент |
|---|---|---|
| Весь сайт | Каждый ответ | Middleware |
| Отдельные представления | Ответ конкретного view | cache_page |
| Фрагменты шаблона | Часть HTML | {% cache %} |
| Произвольные данные | QuerySet, вычисления | cache.set/get |
Разберём каждый.
# 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 декоратор 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.
Самый гибкий уровень — сохранять и получать произвольные данные вручную. Это полезно для тяжёлых 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, а не выполняет код представления при каждом запросе. Проверить это можно несколькими способами.
Самый прямой способ убедиться, что 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
Ещё один удобный способ — посмотреть, выполняется ли запрос к базе данных при повторном открытии страницы.
Открываем страницу первый раз. При первом запросе Django:
- получает HTTP-запрос;
- проверяет наличие страницы в кэше;
- не находит её;
- выполняет код представления;
- делает запросы к базе данных;
- рендерит HTML;
- сохраняет готовый ответ в Redis;
- возвращает HTML пользователю.
При повторном запросе Django:
- получает HTTP-запрос;
- находит готовый ответ в Redis;
- возвращает его пользователю.
Код представления при этом повторно не выполняется. Поэтому при использовании Django Debug Toolbar можно наблюдать следующую картину:
Первый запрос:
SQL queries: несколько запросов
Повторный запрос:
SQL queries: 0
Если при первом открытии страницы выполняются SQL-запросы, а при последующих запросах их количество становится равным нулю, это является хорошим признаком того, что cache_page работает.
На практике наиболее наглядная проверка выглядит так:
- Открыть страницу первый раз.
- Убедиться через Django Debug Toolbar, что выполняются SQL-запросы.
- Обновить страницу.
- Убедиться, что повторных SQL-запросов нет.
- Проверить наличие ключей в Redis через
redis-cli.
Если все эти проверки дают ожидаемый результат, можно с высокой уверенностью утверждать, что кэширование работает.
| Кэшировать | Не кэшировать |
|---|---|
| Список фильмов (меняется редко) | Страница профиля пользователя |
| Страница статистики | Формы (CSRF-токен уникален) |
| Боковая панель жанров | Персонализированные данные |
| Детальная страница фильма | Страницы авторизации |
Главное правило: персональные данные не кэшируются на уровне страницы. Если cache_page применить к странице профиля — первый пользователь, открывший её, закэширует свои данные, и второй пользователь увидит чужой профиль. Фрагментный кэш с переменным ключом ({% cache timeout key user.id %}) — безопасный способ кэшировать части персональных страниц.
# Кэшируется описание запроса, а не данные
cache.set('films', Film.objects.all())
# Кэшируется список объектов
cache.set('films', list(Film.objects.all()))cache_page не учитывает статус авторизации — один ответ для всех. Если страница содержит {% if user.is_authenticated %}, кэшированный HTML первого пользователя (например, авторизованного) покажет другому пользователю (например, анонимному) кнопки, которых он не должен видеть.
Решение: кэшировать только страницы, которые одинаковы для всех пользователей. Для страниц с персонализацией — фрагментный кэш.
По умолчанию если 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 сайт продолжит работать — просто медленнее, без кэша.
- Чем Redis лучше файлового или in-memory кэша Django для продакшн-проекта?
- Почему нельзя положить QuerySet в кэш без явного преобразования в список?
- В чём опасность применения
cache_pageк страницам с персонализированным контентом? - Что такое паттерн cache-aside и как он реализован в нашем представлении
catalog_stats? - Зачем нужна инвалидация кэша и как реализовать её при изменении фильма?
Тип: расширь проект
Часть 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 находится внутри этого блока.