Система кэширования Django

Главная хитрость современных веб сайтов в том, что они динамичные. Каждый раз, когда пользователь запрашивает страницу, веб сервер производит множество действий, от запросов к базе данных до обработки шаблонов, чтобы создать страницу, которую увидит посетитель вашего сайта. Это требует больше затрат, с точки зрения использования ресурсов, чем статичная выдача файла с файловой системы.

Для большинства веб приложений такие лишние вычисления не особо заметны. Эти приложения не являются сайтами washingtonpost.com или slashdot.org. Это сайты небольшого или среднего размера с небольшим трафиком. Но для более крупных сайтов становится очень важна экономия ресурсов.

С этого момента в дело вступает кэширование.

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

given a URL, try finding that page in the cache
if the page is in the cache:
    return the cached page
else:
    generate the page
    save the generated page in the cache (for next time)
    return the generated page

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

Django также может работать с «даунстрим» кэшами, такими как Squid и кэшами браузеров. Это такие типы кэшей, которые вы не можете контролировать напрямую, но можете определять их поведение подсказками (через HTTP заголовки) о том, какую часть вашего сайта следует кэшировать и как.

См.также

Философия дизайна кэширующих фреймворков объясняет некоторые дизайн решения фреймворка.

Настройка кэша

Система кэширования требует небольшой настройки. А именно, надо указать где должны располагаться закэшированные данные – в базе данных, на файловой системе или прямо в памяти. Это важное решение, которое повлияет на производительность вашего кэша. Да, типы кэшей различаются по скорости работы.

Настройки кэша определяются параметром конфигурации CACHES. Ниже приведено объяснение всех возможных значений этого параметра.

Memcached

Самый быстрый и эффективный тип кэша, доступный Django, Memcached является кэшем, который полностью располагается в оперативной памяти, он был разработан для LiveJournal.com и позднее переведён в опенсорс компанией Danga Interactive. Он используется такими сайтами как Facebook и Wikipedia для снижения нагрузки на базу данных и значительного увеличения производительности сайта.

Memcached работает как демон и захватывает определённый объём оперативной памяти. Его задачей является представление быстрого интерфейса для добавления, получения и удаления определённых данных в кэше. Все данные хранятся прямо в оперативной памяти, таким образом нет никакой дополнительной нагрузки на базу данных или файловую систему.

После установкам самого Memcached, следует установить его пакет для Python. Существует несколько таких пакетов; два наиболее используемых — python-memcached и pylibmc.

Для использования Memcached с Django:

  • Установите BACKEND в django.core.cache.backends.memcached.MemcachedCache или django.core.cache.backends.memcached.PyLibMCCache (зависит от выбранного пакета).

  • Определите для LOCATION значение ip:port (где ip — это IP адрес, на котором работает демон Memcached, port — его порт) или unix:path (где path является путём к файлу-сокету Memcached).

В этом примере Memcached запущен на localhost (127.0.0.1) порт 11211, используя python-memcached:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

В этом примере Memcached доступен через локальный файл-сокет /tmp/memcached.sock, используя python-memcached:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': 'unix:/tmp/memcached.sock',
    }
}

При использовании pylibmc, не включайте префикс unix:/:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
        'LOCATION': '/tmp/memcached.sock',
    }
}

Одной из замечательных особенностей Memcached является возможность распределять кэш по нескольким серверам. Это означает, что вы можете запустить демоны Memcached на нескольких машинах и программа будет рассматривать эту группу машин как единый кэш, без необходимости копирования всех значений кэша на каждую машину. Для того, чтобы воспользоваться этой особенностью, укажите адреса всех машин в LOCATION, в виде списка или строки, разделённой запятыми.

В данном примере, кэш распределён по экземплярам Memcached, работающим на IP адресах 172.19.26.240 и 172.19.26.242, на порту 11211 оба:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
            '172.19.26.240:11211',
            '172.19.26.242:11211',
        ]
    }
}

В следующем примере, кэш распределён по экземплярам Memcached, работающим на IP адресах 172.19.26.240 (порт 11211), 172.19.26.242 (порт 11212) и на 172.19.26.244 (порт 11213):

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': [
            '172.19.26.240:11211',
            '172.19.26.242:11212',
            '172.19.26.244:11213',
        ]
    }
}

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

Кэширование в базу данных

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

Для использования таблицы базы данных в качестве бэкэнда кэша:

  • Установите BACKEND в django.core.cache.backends.db.DatabaseCache

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

В данном примере, именем таблицы для кэша будет my_cache_table:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
        'LOCATION': 'my_cache_table',
    }
}

Создание таблицы для кэша

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

python manage.py createcachetable

В результате в базе данных будет создана таблица, структура которой соответствует ожиданиям системы кэширования. Имя для таблицы будет взято из параметра LOCATION.

При использовании нескольких БД кэшей, команда createcachetable создаст по одной таблице для каждого кэша.

Если вы используете множество баз данных, то команда createcachetable обратится к методу allow_migrate() роутера вашей базы данных (см. далее).

Аналогично команде migrate, команда createcachetable не внесёт изменения в существующую таблицу. Она создаёт только отсутствующие таблицы.

Чтобы вывести SQL, который был бы выполнен, без его выполнения, используйте опцию --dry-run.

Множество баз данных

Если у вас несколько баз данных и вы планируете использовать кэширование, потребуется прописать инструкции роутинга для таблицы кэширования. В целях роутинга таблица кэширования представлена моделью CacheEntry в приложении django_cache. Эта модель не отобразится в модельном кэше, но содержимое модели может быть использовано для роутинга.

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

class CacheRouter(object):
    """A router to control all database cache operations"""

    def db_for_read(self, model, **hints):
        "All cache read operations go to the replica"
        if model._meta.app_label == 'django_cache':
            return 'cache_replica'
        return None

    def db_for_write(self, model, **hints):
        "All cache write operations go to primary"
        if model._meta.app_label == 'django_cache':
            return 'cache_primary'
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        "Only install the cache model on primary"
        if app_label == 'django_cache':
            return db == 'cache_primary'
        return None

Если вы не настроите роутинг для кэширования, то модуль кэширования будет использовать базу default.

Естественно, если вы не используете базу данных для кэша, вам не надо беспокоиться об инструкциях роутинга.

Кэширование на файловую систему

Файловый бэкэнд сериализует и сохраняет каждое закэшированное значение в отдельном файле. Для использования этого бэкэнда, установите BACKEND в "django.core.cache.backends.filebased.FileBasedCache", а для LOCATION укажите подходящий каталог. Например, для хранения закэшированных данных в /var/tmp/django_cache, используйте такую настройку:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': '/var/tmp/django_cache',
    }
}

Если вы используете Windows, то подставьте букву диска в начало пути, вот так:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': 'c:/foo/bar',
    }
}

Путь до каталога должен быть абсолютным, т.е. он должен начинаться от корня файловой системы. Завершающий слэш не имеет значения.

Следует удостовериться, что указанный каталог существует и доступен для чтения и записи для пользователя, от которого работает ваш веб сервер. Продолжая предыдущий пример, если ваш веб сервер запущен от пользователя apache, проверьте, что каталог /var/tmp/django_cache существует и доступен для чтения и записи пользователю apache.

Кэширование в оперативной памяти

Это стандартный кэш, который применяется, если другой не определён в вашем файле конфигурации. Если вам требуется высокая скорость работы кэша, но у вас нет возможности развернуть Memcached, рассмотрите вариант использования кэша в оперативной памяти. Этот кэш по-процессный (см. далее) и потокобезопасный. Для его использования надо параметру конфигурации BACKEND присвоить значение "django.core.cache.backends.locmem.LocMemCache". Например:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}

Параметр конфигурации LOCATION используется для идентификации отдельных хранилищ в памяти. Если у вас только один такой кэш, то вы можете пропустить этот параметр. Однако, в случае нескольких кэшей, вам потребуется назначить имя хотя бы для одного из них, для их различия.

Следует отметить, что каждый процесс будет работать со своим собственным экземпляром кэша, что означает невозможность разделения одного кэша между процессами. Это означает, что кэш данного типа неэкономно использует оперативную память и, возможно, не будет хорошим выбором для боевого окружения. Он хорош на время разработки.

Псевдокэширование (для разработки)

Наконец, Django поставляется с «псевдо» кэшем, который не выполняет собственно кэширование. Он просто реализует интерфейс кэша, не делая больше ничего.

Он полезен в случае, когда у вас есть боевой сайт , который плотно использует кэширование в различных местах, а также окружение разработки или тестирование, где кэшировать ничего не надо. Для активации псевдо кэширования, установите BACKEND:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
    }
}

Использование собственного модуля кэширования

Несмотря на то, что Django предоставляет ряд модулей кэширования, вам может потребоваться использовать свой кэширующий модуль. Для подключения внешнего кэширующего модуля используйте путь до него в качестве значения BACKEND параметра конфигурации CACHES, вот так:

CACHES = {
    'default': {
        'BACKEND': 'path.to.backend',
    }
}

При создании своего кэширующего модуля вы можете использовать стандартные модули в качестве примера. Их код располагается в каталоге django/core/cache/backends/ исходного кода Django.

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

Параметры кэша

После определения типа кэша и имени для каждого из них, для каждого модуля можно указать дополнительные аргументы для управления поведением кэша. Эти аргументы представляются в виде дополнительных ключей в параметре конфигурации CACHES. Приведём список допустимых аргументов:

  • TIMEOUT: время устаревания кэша по умолчанию, в секундах. По умолчанию 300 секунд (5 минут). Вы можете установить TIMEOUT в None, тогда кэш никогда не устареет. Если указать 0, все ключи будут сразу устаревать (таким образом можно заставить “не кэшировать”).

  • OPTIONS: Любая опция, которая должна быть передана модулю. Список допустимых опций варьируется от модуля к модулю и передается непосредственно в библиотеку для кэширования.

    Модули кэширования, которые реализуют собственную стратегию очистки (т.е., модули locmem, filesystem и database) учитывают следующие опции:

    • MAX_ENTRIES: максимальное количество элементов в кэше перед началом удаления старых значений. Обычно, 300 элементов.

    • CULL_FREQUENCY: Часть элементов, которые надо удалить при достижении MAX_ENTRIES. Обычное соотношение — 1/CULL_FREQUENCY, таким образом, надо установить CULL_FREQUENCY в 2, чтобы удалять половину значений кэша при достижении MAX_ENTRIES. Аргумент должен быть целым числом и по умолчанию равен 3.

      Значение 0 для CULL_FREQUENCY означает, что весь кэш должен быть сброшен при достижении MAX_ENTRIES. Это делает очистку значительно быстрее для определенных бэкендов(в частности database) ценой увеличения промахов кэша.

  • KEY_PREFIX: Строка, которая автоматически включается (предваряет, по умолчанию) во все ключи кэша, используемые сервером Django.

    Обратитесь к документации на кэш для подробностей.

  • VERSION: Номер версии про умолчанию для ключей кэша, созданных сервером Django.

    Обратитесь к документации на кэш для подробностей.

  • KEY_FUNCTION Строка, содержащая путь до функции, которая определяет правила объединения префикса, версии и ключа в итоговый ключ кэша.

    Обратитесь к документации на кэш для подробностей.

В этом примере, модуль кэширования на файловую систему настроен на таймаут в 60 секунд и ёмкость в 1000 элементов:

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': '/var/tmp/django_cache',
        'TIMEOUT': 60,
        'OPTIONS': {
            'MAX_ENTRIES': 1000
        }
    }
}

Неправильные аргументы игнорируются без ошибок, ка к и неправильные значения поддерживаемых аргументов.

Кэш для каждого сайта

После настройки кэша, простейшим способом его использования будет кэширование всего сайта. Вам надо будет добавить 'django.middleware.cache.UpdateCacheMiddleware' и 'django.middleware.cache.FetchFromCacheMiddleware' в параметр конфигурации MIDDLEWARE_CLASSES, как это показано в примере:

MIDDLEWARE_CLASSES = [
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
]

Примечание

Нет, это не ошибка: мидлварь “update” должна идти первой в списке, а мидлварь “fetch” — последней. Подробности тут не приводятся, обратитесь к Порядку классов MIDDLEWARE, проведенному далее, если они вам нужны.

Затем, добавьте следующие обязательные параметры в файл конфигурации проекта:

  • CACHE_MIDDLEWARE_ALIAS – Метка кэша, используемая для хранилища.

  • CACHE_MIDDLEWARE_SECONDS – Количество секунд хранения каждой закэшированной страницы.

  • CACHE_MIDDLEWARE_KEY_PREFIX – Если кэш разделён между несколькими сайтами внутри одной инсталляции Django, установите в имя сайта или любую другую строку, которая уникальна для этой инсталляции, чтобы предотвратить совпадения. Используйте пустую строку, если это не ваш случай.

Мидлварь FetchFromCacheMiddleware кэширует GET и HEAD отклики со статусом 200, если заголовки запроса и отклика это позволяют. Ответы на запросы для одного URL с разными параметрами запроса считаются уникальными и кэшируются раздельно. Мидлварь кэша ожидает, что запрос HEAD возвращает такие же заголовки, как и соответствующий GET запрос; в этом случае он может вернуть закэшированный GET отклик для запроса HEAD.

Также, мидлварь UpdateCacheMiddleware автоматически устанавливает несколько заголовков в каждом HttpResponse:

  • Устанавливает заголовок Last-Modified с текущими датой и временем при запросе свежей (незакэшированной) версии страницы.

  • Устанавливает в заголовке Expires текущие дату и время, добавляя к ним значение, определённое в CACHE_MIDDLEWARE_SECONDS.

  • Устанавливает заголовок Cache-Control, определяя максимальный возраст для страницы, также используя значение из параметра конфигурации CACHE_MIDDLEWARE_SECONDS.

Обратитесь к Промежуточный слой (Middleware) для подробностей.

Если представление устанавливает собственное время кэширования (т.е. определяет значение max-age в заголовке Cache-Control), то страница будет закэширована на указанное время, вместо CACHE_MIDDLEWARE_SECONDS. Используя декораторы из django.views.decorators.cache, вы можете легко определять время кэширования представлений (с помощью декоратора cache_control) или отключать кэширование для представления (с помощью декоратора never_cache). Обратитесь к разделу Использование других заголовков для получения дополнительной информации об этих декораторах.

Если параметр конфигурации USE_I18N установлен в True, то созданный ключ значения в кэше будет содержать имя активного языка – обратитесь к определению Django языковой настройки). Такое поведение позволяет легко кэшировать мультиязычные сайты, не требуя функционала для создания ключей кэша.

Ключи кэша также включают в себя активный язык в случаях, когда USE_L10N установлен в True, а также включают в себя текущий часовой пояс, если USE_TZ установлен в True.

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

django.views.decorators.cache.cache_page()

Более детальный способ использования системы кэширования возможен за счет кэширования вывода отдельных представлений. Модуль django.views.decorators.cache определяет декоратор cache_page, который автоматически кэширует вывод представления. Использовать его несложно:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
    ...

Декоратор cache_page принимает единственный аргумента: длительность кэширования, в секундах. В предыдущем примере, результат представления my_view() будет закэширован на 15 минут. (Следует отметить, что мы задали значения в виде 60 * 15 в целях читаемости. 60 * 15 будет вычислено в 900, т.е. 15 минут умножается на 60 секунд в минуте.)

Кэш уровня представления, аналогично кэшу уровня сайта, использует ключи на основе URL. Если несколько URL указывают на одно представление, каждый URL будет закэширован отдельно. Продолжая работу с примером my_view, если ваш URLconf выглядит так:

urlpatterns = [
    url(r'^foo/([0-9]{1,2})/$', my_view),
]

то запросы к /foo/1/ и /foo/23/ будут закэшированы отдельно, как вы могли предполагать. Но как только определённый URL (например, /foo/23/) будет запрошен, следующие запросы к этому URL будут использовать кэш.

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

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

Также вы можете переопределять префикс кэша на уровне представления. Декоратор cache_page принимает необязательный именованный аргумент key_prefix, который работает аналогично параметру конфигурации CACHE_MIDDLEWARE_KEY_PREFIX для мидлвари. Он может быть использован следующим образом:

@cache_page(60 * 15, key_prefix="site1")
def my_view(request):
    ...

Аргументы key_prefix и cache можно указать вместе. Аргумент key_prefix и параметр KEY_PREFIX настройки CACHES будут объединены.

Определение кэша уровня представления в URLconf

Примеры из предыдущей секции содержат жёсткое определение кэширования представления, так как декоратор cache_page меняет поведение функции my_view. Такое подход связывает ваше представление с системой кэширования, что не всегда подходит по нескольким причинам. Например, вы можете пожелать распространять представления людям, которые захотят использовать их вне системы кэширования. Решением такой проблемы является указание кэширования в URLconf для каждого представления, а не у каждой функции.

Сделать это несложно: просто оберните функцию представления с помощью cache_page, при обращении к ней в URLconf. Вот так выглядела старая конфигурация URL:

urlpatterns = [
    url(r'^foo/([0-9]{1,2})/$', my_view),
]

Вот так должно быть в случае, когда my_view обёрнута с помощью cache_page:

from django.views.decorators.cache import cache_page

urlpatterns = [
    url(r'^foo/([0-9]{1,2})/$', cache_page(60 * 15)(my_view)),
]

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

Если требуется более тонкий контроль, вы также можете кэшировать фрагменты шаблона с помощью шаблонного тега cache. Для работы данного тега необходимо указать {% load cache %} в начале шаблона.

Шаблонный тег {% cache %} кэширует содержимое блока на указанный период времени. Он принимает как минимум два аргумента: время кэширования в секундах и имя для закэшированного фрагмента. Имя будет использовано как есть, не используйте переменную. Например:

{% load cache %}
{% cache 500 sidebar %}
    .. sidebar ..
{% endcache %}

Временами требуется кэшировать многочисленные копии одного фрагмента, различающиеся только небольшим куском динамических данных. Например, может потребоваться кэширование копии сайдбара из предыдущего примера для каждого пользователя вашего сайта. Это можно сделать, передав дополнительные аргументы в шаблонный тег {% cache %}, что однозначно будет идентифицировать фрагмент:

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. sidebar for logged in user ..
{% endcache %}

Чем больше аргументов вы используете для идентификации фрагмента, тем лучше. Просто указывайте для {% cache %} столько аргументов, сколько считаете нужным.

Если параметр конфигурации USE_I18N установлен в True, то мидлварь кэширования сайта будет учитывать активных язык. Для шаблонного тега cache можно использовать одну из переменных, предназначенных для перевода, доступных шаблонам для достижения того же результата:

{% load i18n %}
{% load cache %}

{% get_current_language as LANGUAGE_CODE %}

{% cache 600 welcome LANGUAGE_CODE %}
    {% trans "Welcome to example.com" %}
{% endcache %}

Время жизни фрагмента в кэше также может быть шаблонной переменной, до тех пор пока эта переменная разрешается в целое значение. Например, если шаблонная переменная my_timeout имеет значение 600, то следующие два примера эквивалентны:

{% cache 600 sidebar %} ... {% endcache %}
{% cache my_timeout sidebar %} ... {% endcache %}

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

По умолчанию, кэширующий тег будет пытаться использовать кэш с именем “template_fragments”. При отсутствии такого кэша, будет происходить переключение на стандартный кэш. Вы можете выбрать использование альтернативного бэкэнда кэша с помощью аргумента using, который должен быть последним аргументом в теге.

{% cache 300 local-thing ...  using="localcache" %}

Указание имени несконфигурированного кэша является ошибкой.

django.core.cache.utils.make_template_fragment_key(fragment_name, vary_on=None)

Чтобы получить ключ кэша для кэшированного фрагменты шаблона, можно использовать функцию make_template_fragment_key. fragment_name – второй аргумент, передаваемый в тег cache, vary_on – список дополнительных аргументов, передаваемых в тег. Эту функцию можно использовать для переназначения или удаления кэшированных фрагментов шаблона:

>>> from django.core.cache import cache
>>> from django.core.cache.utils import make_template_fragment_key
# cache key for {% cache 500 sidebar username %}
>>> key = make_template_fragment_key('sidebar', [username])
>>> cache.delete(key) # invalidates cached template fragment

API низкого уровня для кэширования

Временами кэширование всей созданной страницы не даёт нужного результата и обычно является неудобным излишеством.

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

Для подобных случаев Django предоставляет простой API кэширования низкого уровня. Вы можете использовать этот API для хранения объектов в кэше с любым уровнем детализации. Вы можете поместить в кэш любой объект Python, который может быть безопасно сериализован (“pickled”): строки, словари, списки объектов модели и так далее. (То есть, речь идёт о большинстве объектов языка Python, обратитесь к соответствующей документации.)

Доступ к кэшу

django.core.cache.caches

Вы можете иметь доступ к кэшам, определённым в параметре конфигурации CACHES, через словарно-подобный объект django.core.cache.caches. Повторяющиеся запросы по одинаковому псевдониму в одном и том же потоке будут возвращать одинаковый результат.

>>> from django.core.cache import caches
>>> cache1 = caches['myalias']
>>> cache2 = caches['myalias']
>>> cache1 is cache2
True

Если указанного именованного ключа не существует, то будет вызвано исключение InvalidCacheBackendError.

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

django.core.cache.cache

Для простоты, стандартный кэш доступен через django.core.cache.cache:

>>> from django.core.cache import cache

Этот объект эквивалентен caches['default'].

Использование

Основной интерфейс прост: set(key, value, timeout) и get(key):

>>> cache.set('my_key', 'hello, world!', 30)
>>> cache.get('my_key')
'hello, world!'

Аргумент timeout является необязательным и обычно равен аргументу timeout соответствующего бэкенда кэша из параметре конфигурации CACHES (читайте выше). Аргумент определяет период в секундах, в течение которого значение должно храниться в кэше. Передав None в timeout можно закэшировать данные навсегда. При timeout равном 0 значение никогда не будет кэшироваться.

Если объект отсутствует в кэше, то cache.get() возвращает None:

# Wait 30 seconds for 'my_key' to expire...

>>> cache.get('my_key')
None

Мы не советуем хранить в кэше литеральное значение None, так как вы не сможете определить разницу между сохранённым значением None и отсутствием значения в кэше, о чём сигнализирует возвращение значения None.

Метод cache.get() принимает аргумент default. Он определяет значение, которое будет возвращено, если указанного объекта нет в кэше:

>>> cache.get('my_key', 'has expired')
'has expired'

Для того, чтобы добавить элемент в кэш только в случае, когда его там нет, следует использовать метод add(). Он принимает такие же параметры, как и метод set(), но не будет пытаться изменить значение, если оно уже присутствует в кэше:

>>> cache.set('add_key', 'Initial value')
>>> cache.add('add_key', 'New value')
>>> cache.get('add_key')
'Initial value'

Если вам надо определить сохранил ли метод add() значение в кэше, вы можете проверить значение, которое он возвращает. True указывает, что значение было сохранено, а False – наоборот.

Если вы хотите получить значение ключа, или установить его, если ключа нет в кэше, используйте метод get_or_set(). Принимает аргументы аналогичные get(), но default также записывается в кэш:

>>> cache.get('my_new_key')  # returns None
>>> cache.get_or_set('my_new_key', 'my new value', 100)
'my new value'

Вы также можете передать функцию как значение по умолчанию:

>>> import datetime
>>> cache.get_or_set('some-timestamp-key', datetime.datetime.now)
datetime.datetime(2014, 12, 11, 0, 15, 49, 457920)
Изменено в Django 1.9:

Был добавлен метод get_or_set().

С помощью методы get_many() можно получить ряд значений из кэша за один запрос. Этот метод возвращает словарь со всеми запрошенными ключами, которые действительно присутствуют в кэше (и имеют актуальный срок действия):

>>> cache.set('a', 1)
>>> cache.set('b', 2)
>>> cache.set('c', 3)
>>> cache.get_many(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

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

>>> cache.set_many({'a': 1, 'b': 2, 'c': 3})
>>> cache.get_many(['a', 'b', 'c'])
{'a': 1, 'b': 2, 'c': 3}

Аналогично методу cache.set(), метод set_many() принимает необязательный параметр timeout.

Вы можете явно удалять значения из кэша с помощью метода delete(). Это простейший способ очистки кэша для определённого объекта:

>>> cache.delete('a')

Для одновременного удаления множества значений надо использовать метод delete_many(), который принимает список ключей, подлежащих удалению:

>>> cache.delete_many(['a', 'b', 'c'])

Наконец, для полной очистки кэша надо использовать метод cache.clear(). Будьте осторожными с этим методом. Метод удаляет из кэша всё, не только ключи, установленные вашим приложением.

>>> cache.clear()

Вы также можете увеличивать или уменьшать значение ключа, находящегося в кэше, с помощью методов incr() или desc() соответственно. По-умолчанию, значение в кэше будет увеличено или уменьшено на единицу. Другой шаг изменения величины может быть указан с помощью соответствующего аргумента методов. При попытке изменения несуществующего в кэше значения будет выброшено исключение ValueError.:

>>> cache.set('num', 1)
>>> cache.incr('num')
2
>>> cache.incr('num', 10)
12
>>> cache.decr('num')
11
>>> cache.decr('num', 5)
6

Примечание

Методы incr() и decr() не гарантируют атомарность операции. Тут всё зависит от бэкенда. К примеру, Memcached гарантирует атомарность этих операций. Другие бэкенды делают это через двойную операцию считывания и сохранения нового значения.

Можно закрыть подключение к кэшу методом close(), если бэкенд кэширования предоставляет его.

>>> cache.close()

Примечание

Если кэш не реализует метод close, он будет пустым.

Прификсы ключей кэша

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

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

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

Версионирование кэша

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

Django предоставляет отличный способ выделения отдельных значений в кэше. Система кэширования Django обладает глобальным идентификатором версии, определённым в параметре кэша VERSION. Значение этого параметра автоматически объединяется с префиксом кэша и пользователем, которые предоставил ключ, получая итоговый ключ для обращения к кэшу.

По-умолчанию, запрос любого ключа из кэша автоматически использует текущую версию кэша для сайта. Однако, простейшие функции для работы с кэшем принимают аргумент version и вы можете указывать конкретную версию кэша. Например:

# Set version 2 of a cache key
>>> cache.set('my_key', 'hello world!', version=2)
# Get the default version (assuming version=1)
>>> cache.get('my_key')
None
# Get version 2 of the same key
>>> cache.get('my_key', version=2)
'hello world!'

Версия нужного ключа может быть увеличена или уменьшена с помощью методов incr_version() и decr_version(). Эти методы позволяет изменять версии нужных ключей, не затрагивая остальные ключи. Продолжая наш предыдущий пример:

# Increment the version of 'my_key'
>>> cache.incr_version('my_key')
# The default version still isn't available
>>> cache.get('my_key')
None
# Version 2 isn't available, either
>>> cache.get('my_key', version=2)
None
# But version 3 *is* available
>>> cache.get('my_key', version=3)
'hello world!'

Преобразование ключа кэша

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

def make_key(key, key_prefix, version):
    return ':'.join([key_prefix, str(version), key])

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

Параметр кэша KEY_FUNCTION определяет путь в точечной нотации к функции, соответствующей прототипу make_key() из предыдущего примера. При наличии такой функции она будет использоваться вместо стандартной функции построения итогового ключа.

Предупреждения о проблемах с ключами

Memcached, наиболее используемый в продакшене бэкенд кэширования, не поддерживает ключи длиной более 250 символов или содержащих пробелы или управляющие символы. Использование таких ключей приводит к вызову исключения. Для обеспечения уверенности в портируемости кода между кэширующими бэкендами и для уменьшения количества неприятных сюрпризов, другие встроенные кэширующие бэкенды выбрасывают предупреждение (django.core.cache.backends.base.CacheKeyWarning), если используемый ключ может привести к ошибке при работе с Memcached.

Если вы используете на продакшене кэширующий бэкенд, который может принимать широкий диапазон ключей (собственный бэкенд или один из встроенных бэкендов, кроме Memcached), и не желаете видеть подобные предупреждения, вы можете заглушить CacheKeyWarning с помощью следующего кода в модуле management одного из ваших приложений:

import warnings

from django.core.cache import CacheKeyWarning

warnings.simplefilter("ignore", CacheKeyWarning)

Если вам требуется изменить логику проверки ключа для одного из встроенных бэкендов кэширования, вы можете унаследовать свой класс от него и переопределить метод validate_key. Затем следуйте инструкциям по использованию собственного бэкенда кэширования. Например, для реализации подобного для бэкенда locmem, поместите следующий код в модуль:

from django.core.cache.backends.locmem import LocMemCache

class CustomLocMemCache(LocMemCache):
    def validate_key(self, key):
        """Custom validation, raising exceptions or warnings as needed."""
        # ...

...и используйте путь в точечной нотации к этому классу в ключе BACKEND параметра конфигурации CACHES.

“Даунстрим” кэши

До этого момента данный документ описывал кэширование ваших собственных данных. Но есть ещё один тип кэширования, относящийся к веб разработке: кэширование, выполняемое “даунстрим” кэшами. Это такие системы, которые кэшируют страницы для пользователей так, что они даже не обращаются к вашему сайту.

Рассмотрим несколько примеров таких “даунстрим” кэшей:

  • Ваш провайдер может кэшировать определённые страницы, т.е. если вы запросите страницу с https://example.com/, то провайдер пришлёт её вам без обращения к самому сайту. Основатели данного сайта даже не будут знать о таком кэшировании, так как провайдер находится между вашим браузером и сайтом, незаметно выполняя кэширование.

  • Ваше Django приложение может находится за прокси-кэшем, таким как Squid Web Proxy Cache (http://www.squid-cache.org/), который кэширует страницы для обеспечения производительности. В таком случае, каждый запрос сначала будет обработан прокси, а затем при необходимости будет передан вашему приложению.

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

“Даунстрим” кэширование – неплохой ускоритель, но с ним есть одна проблема: содержимое большинства веб страниц различается в зависимости от аутентификации и других настроек и кэширующие системы, которые слепо сохраняют страницы, основываясь в основном на URL, могут выдать неправильные или секретные данные следующим посетителям этих страниц.

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

К счастью, протокол HTTP предоставляет решение для этой проблемы. Существует ряд HTTP заголовков, которые инструктируют “даунстрим” кэши как именно следует различать страницы в зависимости от определённых переменных, также указывая что некоторые страницы кэшировать вообще не следует. Мы рассмотрим некоторые из этих заголовков в следующих секциях.

Использование заголовков Vary

Заголовок Vary определяет какие именно заголовки запроса кэширующий механизм должен принимать во внимание при построении своего ключа. Например, если содержимое страницы зависит от языковой настройки, то страница говорит “обрати внимание на язык.”

По-умолчанию, кэширующая система Django создаёт свои ключи, используя запрошенный путь и запрос, т.е., "https://www.example.com/stories/2005/?order_by=author". Это означает, что каждый запрос по этому URL-у будет использовать одну закэшированную версию, независимо от различия в куках или языковых настройках. Однако, если эта страница создаёт своё содержимое, основываясь на разнице в заголовках запроса, таких как куки или язык или тип браузера, вам следует использовать заголовок Vary, чтобы указать кэширующему механизму, что вывод данной страница зависит от этих параметров.

Чтобы сделать это в Django следует использовать декоратор представления django.views.decorators.vary.vary_on_headers(), так:

from django.views.decorators.vary import vary_on_headers

@vary_on_headers('User-Agent')
def my_view(request):
    # ...

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

Преимущество использования декоратора vary_on_headers по сравнению с ручной установкой заголовка Vary (с помощью response['Vary'] = 'user-agent') в том, что декоратор добавляет значение к заголовку Vary (который может уже существовать), а не переопределяет его.

Вы можете передавать несколько заголовков в vary_on_headers():

@vary_on_headers('User-Agent', 'Cookie')
def my_view(request):
    # ...

Это указывает “даунстрим” кэшам, что надо обращать внимание на оба, т.е. любая комбинация типа браузера и куки будет кэшироваться отдельно. Например, запрос от Mozilla с кукой foo=bar будет рассматриваться как отличный от запроса от Mozilla с кукой foo=ham.

Так как зависимость от куки является настолько стандартной, что существует декоратор django.views.decorators.vary.vary_on_cookie(). Следующие два представления эквивалентны:

@vary_on_cookie
def my_view(request):
    # ...

@vary_on_headers('Cookie')
def my_view(request):
    # ...

Заголовки, которые вы передаёте в vary_on_headers нечувствительны к регистру, т.е. "User-Agent" аналогичен "user-agent".

Также вы можете использовать вспомогательную функцию django.utils.cache.patch_vary_headers() напрямую. Эта функция устанавливает или добавляет заголовок Vary. Например:

from django.utils.cache import patch_vary_headers

def my_view(request):
    # ...
    response = render_to_response('template_name', context)
    patch_vary_headers(response, ['Cookie'])
    return response

Функция patch_vary_headers принимает экземпляр HttpResponse в качестве первого аргумента и список/кортеж имён заголовков в качестве второго аргумента.

Более подробно о заголовках Vary можно почитать в официальной спецификации.

Управление кэшированием: Использование других заголовков

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

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

Решением является указание того, что страница должна кэшироваться в личном кэше. Для этого в Django следует использовать декоратор представления cache_control. Например:

from django.views.decorators.cache import cache_control

@cache_control(private=True)
def my_view(request):
    # ...

Этот декоратор обеспечивает автоматическую отправку соответствующих HTTP заголовков.

Следует отметить, что настройки открытости и закрытости взаимоисключающие. Декоратор обеспечивает эту проверку. Примером использования таких настроек будет блог, который содержит закрытые и открытые записи. Открытые записи могут кэшироваться везде. Следующий код использует django.utils.cache.patch_cache_control(), ручной способ изменения управляющего заголовка (обычно эта функция вызывается в декораторе cache_control):

from django.views.decorators.cache import patch_cache_control
from django.views.decorators.vary import vary_on_cookie

@vary_on_cookie
def list_blog_entries_view(request):
    if request.user.is_anonymous():
        response = render_only_public_entries()
        patch_cache_control(response, public=True)
    else:
        response = render_private_and_public_entries(request.user)
        patch_cache_control(response, private=True)

    return response

Существует несколько других способов управления параметрами кэширования. Например, протокол HTTP позволяет приложениям делать следующее:

  • Определить максимальное время хранения страницы в кэше.

  • Указать, должен ли кэш всегда проверять наличие новых версий, возвращая закэшированное содержимое только если не было никаких изменений. (Некоторые кэши могут отдавать закэшированное содержимое даже если изменения страницы обнаружены, просто потому что ещё не вышло время жизни закэшированной записи.)

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

from django.views.decorators.cache import cache_control

@cache_control(must_revalidate=True, max_age=3600)
def my_view(request):
    # ...

Любая верная HTTP директива Cache-Control верна в cache_control(). Приведём полный список:

  • public=True
  • private=True
  • no_cache=True
  • no_transform=True
  • must_revalidate=True
  • proxy_revalidate=True
  • max_age=num_seconds
  • s_maxage=num_seconds

Больше об этих директивах можно узнать в спецификации на Cache-Control.

(Следует отметить, что кэширующая мидлварь уже устанавливает заголовок max-age равным значению параметра конфигурации CACHE_MIDDLEWARE_SECONDS. Если вы используете определённый max-age в декораторе cache_control, декоратор применит его, правильно объединив значения заголовка.)

Если потребуется использовать заголовки, которые полностью отключают кэширование, то воспользуйтесь декоратором django.views.decorators.cache.never_cache(). Например:

from django.views.decorators.cache import never_cache

@never_cache
def myview(request):
    # ...

Порядок записей в MIDDLEWARE_CLASSES

Если вы используете кэширующую мидлварь, важно правильно её разместить в параметре конфигурации MIDDLEWARE_CLASSES. Так происходит из-за того, что кэширующая мидлварь должна знать какие заголовки как влияют на кэширующие системы. Мидлварь всегда добавляет что-то в заголовок Vary, когда может.

UpdateCacheMiddleware работает во время выдачи отклика, там где мидлвари применяются с конца списка, т.е. элемент наверху списка выполняется последним. Таким образом, надо удостовериться, что UpdateCacheMiddleware указан до любых мидлварей, которые могут добавить что-то в заголовок Vary. Следующие модули как раз такие:

  • SessionMiddleware добавляет Cookie

  • GZipMiddleware добавляет Accept-Encoding

  • LocaleMiddleware добавляет Accept-Language

FetchFromCacheMiddleware, с другой стороны, выполняется во время запроса, когда мидлвари применяются от начала списка, т.е. элемент наверху списка выполняется первым. FetchFromCacheMiddleware также должен выполняться после других мидлварей, которые изменяют заголовок Vary.