Данила Пантелеев
Django2026-07-2414 мин чтения17

Кэширование в Django: Redis, Memcached и кэширование фрагментов шаблонов

Кэширование в Django: Redis, Memcached и кэширование фрагментов шаблонов

Кэширование в 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: запрос проверяет кэш и лишь при промахе идёт в базу данных

В статье разберём весь стек кэширования 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:

python
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:

БэкендКлассКогда использовать
RedisRedisCacheпродакшен, выбор по умолчанию
MemcachedPyMemcacheCache, PyLibMCCacheпродакшен, простые сценарии
База данныхDatabaseCacheмаленькие проекты без выделенного сервиса
Файловая системаFileBasedCacheредко; есть нюансы безопасности
Память процессаLocMemCacheлокальная разработка
ЗаглушкаDummyCacheтесты: API есть, кэша нет

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

Redis как кэш-бэкенд Django 5.1

Начиная с Django 4.0 в коробке есть нативный RedisCache – сторонние пакеты вроде django-redis больше не обязательны. Нужен только клиент redis-py:

bash
pip install "redis>=4.2"

Готовый конфиг для продакшена:

python
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).

bash
pip install pymemcache

Конфиг по TCP и по unix-сокету (сокет быстрее на одной машине):

python
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 по хэшу ключа):

python
'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 как кэш-бэкендов Django: персистентность, структуры данных, отказоустойчивость

КритерийRedisMemcached
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 – последним.

python
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:

python
from django.views.decorators.cache import cache_page @cache_page(60 * 15) # 15 минут def article_list(request): ...

Тот же эффект в urls.py – удобно для сторонних и классовых view, которые не хочется оборачивать в коде:

python
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 можно направить в конкретный кэш:

python
@cache_page(60 * 15, cache='pages') def article_list(request): ...

Ключевой риск – заголовки Vary. cache_page формирует ключ с учётом заголовков, перечисленных в Vary ответа. Если middleware, работающее с cookie (например, SessionMiddleware при обращении к сессии или CsrfViewMiddleware), выставляет Vary: Cookie, каждая сессия получит свою копию страницы – кэш раздувается, а hit rate стремится к нулю. В худшем случае без корректного Vary персонализированная страница одного пользователя будет показана другому.

Для тонкой настройки:

python
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 %} оборачивает фрагмент шаблона:

django
{% load cache %} {% cache 500 sidebar %} <aside> {% for category in categories %} <a href="{{ category.get_absolute_url }}">{{ category.name }}</a> {% endfor %} </aside> {% endcache %}

Аргументы после имени фрагмента – vary-параметры: для каждой комбинации значений создаётся своя копия. Вариация по пользователю, GET-параметру, языку:

django
{% cache 600 sidebar request.user.username %} ... {% endcache %} {% cache 300 product_list request.GET.page %} ... {% endcache %}

Типовые кандидаты: сайдбар, меню категорий, блок «популярное», футер, облако тегов – всё, что дорого рендерить (десятки запросов, тяжёлые циклы) и редко меняется. Часто один {% cache %} вокруг меню категорий убирает 20–30 запросов с каждой страницы сайта.

Если фрагментов много, имеет смысл выделить им отдельный алиас с собственной политикой:

python
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 }, }
django
{% 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.

python
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'])

Типовой паттерн «кэшируй результат запроса»:

python
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

Два нюанса сериализации:

  1. Django сериализует значения через pickle – можно класть почти любые Python-объекты. Но pickle выполняет код при десериализации, поэтому данные из кэша нельзя считать доверенными, если к хранилищу есть доступ у недоверенных процессов.
  2. Явно контролируйте момент вычисления QuerySet. Pickle при записи форсирует вычисление «ленивого» QuerySet, поэтому чтение из кэша SQL-запросов уже не вызовет – но сам запрос окажется скрыт внутри cache.set(), а в кэш попадёт весь вычисленный набор целиком. Оборачивайте результат в list() (или сериализуйте в словари): так вы явно управляете и моментом вычисления, и размером данных, уходящих в кэш.

Отдельный класс задач для низкоуровневого API – кэширование ответов внешних сервисов. Вызовы сторонних API (платёжные шлюзы, CRM, геосервисы) дороги по времени и часто лимитированы по числу запросов, поэтому их результаты выгодно держать в кэше с TTL от минут до часов. О типовых сценариях таких интеграций мы писали в статье «Интеграция WordPress с российскими CRM: Битрикс24, amoCRM и МойСклад».

Инвалидация кэша: как не показывать устаревшие данные

Есть две стратегии, и в зрелом проекте они комбинируются:

  • TTL – данные протухают сами через N секунд. Просто, но до конца TTL пользователи видят старьё.
  • Явная инвалидация – при изменении данных ключ удаляется принудительно.

Для фрагментов шаблонов ключ строится через make_template_fragment_key:

python
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)

Инвалидация по сигналам – стандартный способ держать кэш консистентным:

python
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):

python
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 с хэшированием:

python
# 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).

Как измерить эффект от кэширования

Внедрение без замеров – гадание. Порядок действий:

  1. Зафиксируйте базовую линию: число SQL-запросов и время их выполнения на целевых страницах (django-debug-toolbar или логирование connection.queries), TTFB из мониторинга, текущий RPS до деградации.
  2. Включите кэш поэтапно – сначала фрагменты, потом per-view, потом низкоуровневый API на горячих точках. Замеряйте после каждого шага.
  3. Следите за hit rate: в Redis – INFO stats (keyspace_hits / keyspace_misses), в Memcached – stats (get_hits / get_misses). Hit rate считается как hits / (hits + misses); ниже 80–90% для страничного кэша – повод разбираться с Vary и TTL.
  4. Прогоните нагрузочный тест (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_deletetransaction.on_commit() там, где есть транзакции), версионирование, – а не после первой жалобы на устаревшие данные. Кэш, который нельзя предсказуемо сбросить, хуже отсутствия кэша.

Чек-лист внедрения кэширования в Django: бэкенд, уровни кэширования, инвалидация, замеры

Автор: Данила Пантелеев · 2026-07-24

Нужна помощь с реализацией?

Описывайте задачу – разберём что подойдёт под ваш проект

Написать в TelegramЗаказать на Kwork

Ещё статьи

WordPress + Telegram: как автоматизировать заявки, уведомления и поддержку клиентов
Плагины2026-07-3016 мин

WordPress + Telegram: как автоматизировать заявки, уведомления и поддержку клиентов

WordPress + Telegram: как автоматизировать заявки, уведомления и поддержку клиентов Заявка с сайта приходит на почту. Менеджер замечает письмо через два часа, перезванивает – а клиент уже оставил...

Читать
Безопасная авторизация в WordPress: 2FA, SSO, reCAPTCHA и защита от брутфорса
Безопасность2026-07-2119 мин

Безопасная авторизация в WordPress: 2FA, SSO, reCAPTCHA и защита от брутфорса

Безопасная авторизация в WordPress: 2FA, SSO, reCAPTCHA и защита от брутфорса Введение: почему форма входа – постоянная мишень ботов WordPress работает примерно на 43% всех сайтов интернета, и это...

Читать
Интеграция WordPress с российскими CRM: Битрикс24, amoCRM и МойСклад
Разработка2026-07-0617 мин

Интеграция WordPress с российскими CRM: Битрикс24, amoCRM и МойСклад

Как интегрировать WordPress с Битрикс24, amoCRM и МойСклад через плагины, REST API и вебхуки. Примеры PHP-кода, ограничения API, безопасность и чек-лист внедрения.

Читать