
Кэширование в Django: Redis, Memcached и кэширование фрагментов шаблонов Введение: зачем Django-проекту кэширование Симптомы знакомы каждому, кто выкатывал Django под реальную нагрузку: страница...
Кэширование в Django: Redis, Memcached и кэширование фрагментов шаблонов
Введение: зачем Django-проекту кэширование
Симптомы знакомы каждому, кто выкатывал Django под реальную нагрузку: страница каталога делает 80–120 SQL-запросов, TTFB ползёт к секунде, база данных утилизирована на 90%, хотя 95% запросов возвращают одни и те же данные. Контентные страницы, списки категорий, блоки «популярное» – всё это рендерится заново на каждый хит, хотя меняется раз в час или раз в день.
Кэширование в Django решает ровно эту проблему: результат дорогой операции (SQL-запрос, рендеринг шаблона, внешний API-вызов) сохраняется в быстром хранилище, и следующие запросы получают готовый ответ за миллисекунды. На практике грамотное кэширование даёт:
- снижение числа SQL-запросов на горячих страницах в 5–20 раз;
- рост RPS без добавления серверов;
- сокращение TTFB с сотен миллисекунд до десятков.

В статье разберём весь стек кэширования Django 5.1: выбор бэкенда (Redis vs Memcached) с готовыми конфигурациями, четыре уровня гранулярности – от кэширования всего сайта до низкоуровневого API, стратегии инвалидации и типовые грабли, которые отнимают у команд больше всего времени. Все примеры актуальны для Django 5.1; там, где возможность появилась раньше (нативный RedisCache – с Django 4.0, touch() – с 4.2), версия указана явно. Материал рассчитан на backend-разработчиков и техлидов; для руководителей, оценивающих стоимость внедрения, полезен блок про измерение эффекта.
Как устроен фреймворк кэширования в Django
Ключевая идея: единый API независимо от бэкенда. Код cache.get('key') / cache.set('key', value, timeout) одинаково работает с Redis, Memcached, базой данных или файловой системой. Смена хранилища сводится к правке одной настройки – код приложения не трогаем.
Настройка живёт в settings.py в словаре CACHES:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'TIMEOUT': 300, # TTL по умолчанию, секунды
'KEY_PREFIX': 'mysite', # префикс всех ключей проекта
'VERSION': 1, # версия ключей (см. инвалидацию)
'OPTIONS': {},
}
}Встроенные бэкенды Django 5.1:
| Бэкенд | Класс | Когда использовать |
|---|---|---|
| Redis | RedisCache | продакшен, выбор по умолчанию |
| Memcached | PyMemcacheCache, PyLibMCCache | продакшен, простые сценарии |
| База данных | DatabaseCache | маленькие проекты без выделенного сервиса |
| Файловая система | FileBasedCache | редко; есть нюансы безопасности |
| Память процесса | LocMemCache | локальная разработка |
| Заглушка | DummyCache | тесты: API есть, кэша нет |
Два фундаментальных механизма, на которых держится всё управление кэшем: TIMEOUT (время жизни записи, после которого она считается протухшей) и версионирование ключей (номер версии входит в состав ключа, поэтому инкремент версии делает недоступной всю группу старых записей без их физического удаления). К ним вернёмся в разделе про инвалидацию.
Redis как кэш-бэкенд Django 5.1
Начиная с Django 4.0 в коробке есть нативный RedisCache – сторонние пакеты вроде django-redis больше не обязательны. Нужен только клиент redis-py:
pip install "redis>=4.2"Готовый конфиг для продакшена:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'TIMEOUT': 300,
'KEY_PREFIX': 'mysite',
'OPTIONS': {
'pool_class': 'redis.connection.BlockingConnectionPool',
'socket_timeout': 2,
'socket_connect_timeout': 2,
},
}
}Практические замечания:
- Номер БД (
/1в URL) – выделяйте кэшу отдельную логическую базу, не ту, где живут сессии или брокер Celery. Так проще смотреть статистику и делатьFLUSHDBпри отладке. - Персистентность: включите RDB-снапшоты или AOF в
redis.conf, и кэш переживёт рестарт демона. Для дорогих вычислений это критично – прогрев кэша с нуля на большом проекте может занять десятки минут повышенной нагрузки на БД. - maxmemory-policy: задайте
allkeys-lru, чтобы Redis при нехватке памяти вытеснял давно не используемые ключи, а не отклонял записи. - Отказоустойчивость: репликация + Sentinel или Redis Cluster закрывают сценарий отказа узла.
Минусы: на простых get/set latency чуть выше, чем у Memcached (разница – доли миллисекунды, на большинстве проектов не заметна), и Redis требует минимальной эксплуатационной дисциплины (память, персистентность, мониторинг).
Memcached как кэш-бэкенд
Django поддерживает два клиента: PyMemcacheCache (рекомендуемый, клиент pymemcache) и PyLibMCCache (обёртка над libmemcached, клиент pylibmc).
pip install pymemcacheКонфиг по TCP и по unix-сокету (сокет быстрее на одной машине):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
# TCP:
'LOCATION': '127.0.0.1:11211',
# либо unix-сокет:
# 'LOCATION': 'unix:/tmp/memcached.sock',
'TIMEOUT': 300,
'OPTIONS': {
'no_delay': True,
'connect_timeout': 1,
'timeout': 1,
},
}
}Несколько серверов – просто список; Django сам распределяет ключи по ним (sharding по хэшу ключа):
'LOCATION': [
'10.0.0.10:11211',
'10.0.0.11:11211',
'10.0.0.12:11211',
]Важно понимать: инстансы Memcached не общаются между собой. Это не кластер – клиентская библиотека решает, на какой сервер писать ключ. Падение одного сервера = потеря его доли ключей.
Плюсы: минимальная latency, предельно простая модель, многопоточность из коробки. Минусы существеннее:
- нет персистентности – рестарт демона обнуляет кэш целиком, и вся нагрузка мгновенно падает на БД (thundering herd);
- нет репликации;
- лимит значения 1 МБ (для HTML-фрагментов хватает, для крупных сериализованных структур – уже нет).
Redis vs Memcached: матрица выбора

| Критерий | Redis | Memcached |
|---|---|---|
| Latency на get/set | низкая; на простых операциях чуть выше, чем у Memcached | минимальная; разница с Redis – доли миллисекунды |
| Персистентность | RDB/AOF | нет |
| Структуры данных | строки, хэши, списки, множества, sorted sets | только строки |
| Отказоустойчивость | репликация, Sentinel, Cluster | нет (sharding на клиенте) |
| Лимит значения | 512 МБ | 1 МБ |
| Память на запись | выше (метаданные типов) | ниже |
| Поддержка в Django 5.1 | нативный бэкенд | нативный бэкенд |
| Сессии, Celery-брокер, rate limiting | да, один сервис на всё | только кэш |
Конкретные цифры latency намеренно не приводим: они сильно зависят от сети, железа и нагрузки, и любые «средние миллисекунды» без методики замера вводят в заблуждение. Замеряйте на своём стенде – через redis-cli --latency или профилировщик.
Типовые сценарии:
- «Просто кэш страниц и фрагментов, данных не жалко» – Memcached достаточно и чуть быстрее. Классический сценарий, для которого его и создавали.
- «Кэш + сессии + брокер Celery на одном сервере» – Redis. Одна инфраструктура вместо трёх, меньше эксплуатационных затрат.
- «Нельзя терять кэш при рестарте» (дорогие вычисления, долгий прогрев) – Redis с включённой персистентностью.
- «Горизонтальное масштабирование простым способом» – несколько инстансов Memcached со sharding'ом по ключам работает из коробки.
Рекомендация по умолчанию для нового проекта на Django 5.1 – Redis: нативная поддержка, персистентность, один сервис закрывает кэш, сессии и очереди.
Уровень 1: кэширование всего сайта через middleware
Самый грубый уровень – кэшировать все GET/HEAD-ответы целиком. Для этого служит пара middleware: UpdateCacheMiddleware (сохраняет ответ) и FetchFromCacheMiddleware (отдаёт из кэша). Порядок критичен: update – первым, fetch – последним.
MIDDLEWARE = [
'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# ... остальные middleware ...
'django.middleware.cache.FetchFromCacheMiddleware',
]
CACHE_MIDDLEWARE_SECONDS = 600
CACHE_MIDDLEWARE_KEY_PREFIX = 'mysite'Применимость узкая: полностью анонимный контентный сайт без персонализации. Страницы с CSRF-токенами, сессиями, корзиной, «Здравствуйте, Иван» кэшировать целиком нельзя – получите утечку данных между пользователями. На большинстве реальных проектов этот уровень пропускают и сразу идут к per-view или фрагментам.
Уровень 2: кэширование представлений через cache_page
Декоратор @cache_page кэширует ответ конкретного view:
from django.views.decorators.cache import cache_page
@cache_page(60 * 15) # 15 минут
def article_list(request):
...Тот же эффект в urls.py – удобно для сторонних и классовых view, которые не хочется оборачивать в коде:
from django.urls import path
from django.views.decorators.cache import cache_page
urlpatterns = [
path('articles/', cache_page(60 * 15)(ArticleListView.as_view())),
]Если в CACHES несколько алиасов, view можно направить в конкретный кэш:
@cache_page(60 * 15, cache='pages')
def article_list(request):
...Ключевой риск – заголовки Vary. cache_page формирует ключ с учётом заголовков, перечисленных в Vary ответа. Если middleware, работающее с cookie (например, SessionMiddleware при обращении к сессии или CsrfViewMiddleware), выставляет Vary: Cookie, каждая сессия получит свою копию страницы – кэш раздувается, а hit rate стремится к нулю. В худшем случае без корректного Vary персонализированная страница одного пользователя будет показана другому.
Для тонкой настройки:
from django.views.decorators.cache import cache_control, cache_page
from django.views.decorators.vary import vary_on_headers
# cache_page должен быть внешним декоратором: он читает Vary
# в момент сохранения ответа в кэш, и vary_on_headers должен
# успеть пропатчить заголовок до этого.
@cache_page(60 * 15)
@vary_on_headers('Accept-Language')
def article_list(request):
...
# Страницы с персональными данными – не кэшировать на сервере вообще:
@cache_control(private=True, max_age=3600)
def dashboard(request):
...Правило безопасности: cache_page применяйте только к view, ответ которых одинаков для всех пользователей (или различается строго по списку vary_on_headers).
Уровень 3: кэширование фрагментов шаблонов ({% cache %})
Самый полезный уровень для сайтов с персонализацией: страница в целом динамическая, но отдельные дорогие блоки – нет. Тег {% cache %} оборачивает фрагмент шаблона:
{% load cache %}
{% cache 500 sidebar %}
<aside>
{% for category in categories %}
<a href="{{ category.get_absolute_url }}">{{ category.name }}</a>
{% endfor %}
</aside>
{% endcache %}Аргументы после имени фрагмента – vary-параметры: для каждой комбинации значений создаётся своя копия. Вариация по пользователю, GET-параметру, языку:
{% cache 600 sidebar request.user.username %}
...
{% endcache %}
{% cache 300 product_list request.GET.page %}
...
{% endcache %}Типовые кандидаты: сайдбар, меню категорий, блок «популярное», футер, облако тегов – всё, что дорого рендерить (десятки запросов, тяжёлые циклы) и редко меняется. Часто один {% cache %} вокруг меню категорий убирает 20–30 запросов с каждой страницы сайта.
Если фрагментов много, имеет смысл выделить им отдельный алиас с собственной политикой:
CACHES = {
'default': {...},
'template_fragments': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/2',
'TIMEOUT': 600,
# Лимит памяти для Redis задаётся на стороне сервера
# (maxmemory и maxmemory-policy в redis.conf), а не через OPTIONS
},
}{% cache 600 sidebar using='template_fragments' %}
...
{% endcache %}Отдельный алиас позволяет сбрасывать фрагменты (FLUSHDB на базе 2), не трогая остальной кэш, и настраивать им собственный TIMEOUT. Обратите внимание: параметр MAX_ENTRIES, который иногда копируют из примеров для локального кэша, поддерживается только бэкендами LocMemCache, FileBasedCache и DatabaseCache. Для RedisCache он не работает – объём и политика вытеснения настраиваются на стороне сервера Redis через maxmemory и maxmemory-policy (см. раздел про Redis выше).
Уровень 4: низкоуровневый API django.core.cache
Полный контроль: кэшируем не ответы и не HTML, а сами данные – результаты запросов, вычисления, ответы внешних API.
from django.core.cache import cache
# Базовые операции
cache.set('products:featured', products, timeout=3600)
products = cache.get('products:featured', default=[])
cache.add('lock:export', True, timeout=60) # запишет, только если ключа нет
cache.delete('products:featured')
cache.touch('products:featured', timeout=3600) # продлить TTL (появился в Django 4.2)
cache.incr('stats:hits') # атомарный инкремент
cache.clear() # сбросить всё (осторожно)
# Самая частая операция – get_or_set:
products = cache.get_or_set(
'products:featured',
lambda: list(Product.objects.filter(is_featured=True)),
timeout=3600,
)
# Пакетные операции – один round-trip вместо N:
cache.set_many({'a': 1, 'b': 2}, timeout=300)
data = cache.get_many(['a', 'b'])
cache.delete_many(['a', 'b'])Типовой паттерн «кэшируй результат запроса»:
from django.core.cache import cache
def get_category_tree():
tree = cache.get('catalog:category_tree')
if tree is None:
tree = build_category_tree() # тяжёлый запрос с рекурсией
cache.set('catalog:category_tree', tree, timeout=3600)
return treeДва нюанса сериализации:
- Django сериализует значения через pickle – можно класть почти любые Python-объекты. Но pickle выполняет код при десериализации, поэтому данные из кэша нельзя считать доверенными, если к хранилищу есть доступ у недоверенных процессов.
- Явно контролируйте момент вычисления QuerySet. Pickle при записи форсирует вычисление «ленивого» QuerySet, поэтому чтение из кэша SQL-запросов уже не вызовет – но сам запрос окажется скрыт внутри
cache.set(), а в кэш попадёт весь вычисленный набор целиком. Оборачивайте результат вlist()(или сериализуйте в словари): так вы явно управляете и моментом вычисления, и размером данных, уходящих в кэш.
Отдельный класс задач для низкоуровневого API – кэширование ответов внешних сервисов. Вызовы сторонних API (платёжные шлюзы, CRM, геосервисы) дороги по времени и часто лимитированы по числу запросов, поэтому их результаты выгодно держать в кэше с TTL от минут до часов. О типовых сценариях таких интеграций мы писали в статье «Интеграция WordPress с российскими CRM: Битрикс24, amoCRM и МойСклад».
Инвалидация кэша: как не показывать устаревшие данные
Есть две стратегии, и в зрелом проекте они комбинируются:
- TTL – данные протухают сами через N секунд. Просто, но до конца TTL пользователи видят старьё.
- Явная инвалидация – при изменении данных ключ удаляется принудительно.
Для фрагментов шаблонов ключ строится через make_template_fragment_key:
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
def invalidate_sidebar(username):
key = make_template_fragment_key('sidebar', [username])
cache.delete(key)Инвалидация по сигналам – стандартный способ держать кэш консистентным:
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from .models import Category
@receiver([post_save, post_delete], sender=Category)
def invalidate_category_tree(sender, **kwargs):
cache.delete('catalog:category_tree')
cache.delete_many([
'catalog:menu',
'catalog:popular_categories',
])Практический нюанс: post_save срабатывает до коммита транзакции. Если изменение данных обёрнуто в transaction.atomic(), инвалидацию стоит откладывать через transaction.on_commit() – иначе кэш может быть сброшен и тут же перезаписан старыми данными из ещё не закоммиченной транзакции.
Версионирование – элегантный «массовый сброс» без удаления ключей. Параметр VERSION в CACHES или аргумент version в вызовах API входит в состав ключа; инкремент версии мгновенно делает все старые записи недоступными (они просто протухнут по TTL):
cache.set('menu', html, version=2)
cache.get('menu', version=2) # другая версия = другой ключcache.clear() на проде – крайняя мера: резкий прогрев всего кэша под нагрузкой может положить БД сильнее, чем отсутствие кэша вообще.
Типовые грабли и как их обойти
Падение Memcached = потеря всех данных. Вся нагрузка мгновенно уходит в БД (thundering herd). Смягчение: использовать get_or_set (атомарен, меньше дублирующих вычислений), не ставить одинаковый TTL на массово записываемые ключи (добавляйте джиттер timeout + random.randint(0, 60)), держать запас мощности БД на прогрев.
CacheKeyWarning и лимит 250 символов. Memcached отклоняет ключи длиннее 250 байт и ключи с пробелами/управляющими символами. Django выдаёт CacheKeyWarning, но в боевом режиме запись молча не попадёт в кэш. Лечится либо дисциплиной имён ключей, либо своей KEY_FUNCTION с хэшированием:
# myproject/cache_utils.py
import hashlib
def hashed_key(key, key_prefix, version):
digest = hashlib.md5(key.encode(), usedforsecurity=False).hexdigest()
return f'{key_prefix}:{version}:{digest}'
# settings.py
CACHES = {
'default': {
# ...
'KEY_FUNCTION': 'myproject.cache_utils.hashed_key',
}
}Pickle-безопасность. Десериализация pickle выполняет произвольный код. Следствия: Redis/Memcached не должны быть доступны извне (bind на loopback/private network, AUTH для Redis), а FileBasedCache – ни в коем случае не в каталоге, доступном веб-серверу на раздачу.
Vary: Cookie и утечка персональных данных. Если cache_page применён к view, ответ которого зависит от пользователя, а Vary настроен неверно, один пользователь увидит чужую страницу. Диагностика: смотрите реальные заголовки Vary в ответе (curl -I), проверяйте hit rate – аномально низкий обычно означает, что в Vary затесались Cookie.
LocMemCache на проде. Частая история: локально «всё работало», на проде «инвалидация не срабатывает». Причина – gunicorn запускает несколько процессов, и у каждого свой LocMemCache в памяти: cache.delete() в одном процессе не виден остальным. На проде – только внешнее хранилище (Redis/Memcached).
Как измерить эффект от кэширования
Внедрение без замеров – гадание. Порядок действий:
- Зафиксируйте базовую линию: число SQL-запросов и время их выполнения на целевых страницах (django-debug-toolbar или логирование
connection.queries), TTFB из мониторинга, текущий RPS до деградации. - Включите кэш поэтапно – сначала фрагменты, потом per-view, потом низкоуровневый API на горячих точках. Замеряйте после каждого шага.
- Следите за hit rate: в Redis –
INFO stats(keyspace_hits/keyspace_misses), в Memcached –stats(get_hits/get_misses). Hit rate считается какhits / (hits + misses); ниже 80–90% для страничного кэша – повод разбираться сVaryи TTL. - Прогоните нагрузочный тест (locust, wrk, ab) до и после: сравнение RPS и перцентилей latency на одном железе – самый убедительный аргумент.
Для бизнеса эффект конвертируется в деньги напрямую: снижение нагрузки на БД откладывает вертикальное масштабирование (переход на более дорогой инстанс БД – обычно самая затратная строка инфраструктуры), а рост RPS на том же железе отодвигает покупку новых серверов приложений. Типовой результат на контентных и каталожных проектах – снижение числа запросов к БД в 5–10 раз и рост выдерживаемого RPS в 3–5 раз без смены железа.
Заключение
Короткое резюме:
- Бэкенд по умолчанию для Django 5.1 – Redis: нативная поддержка, персистентность, один сервис для кэша, сессий и Celery. Memcached оправдан для простых сценариев, где важна минимальная latency, а потеря кэша при рестарте не критична.
- Порядок внедрения: начните с
{% cache %}вокруг самых дорогих блоков шаблонов (меню, сайдбары) – это дешевле всего и безопаснее; затемcache_pageдля анонимных страниц; затем низкоуровневый API для горячих запросов и внешних вызовов. - Главное правило: продумайте инвалидацию до включения кэша – TTL, сигналы
post_save/post_delete(сtransaction.on_commit()там, где есть транзакции), версионирование, – а не после первой жалобы на устаревшие данные. Кэш, который нельзя предсказуемо сбросить, хуже отсутствия кэша.




