Срок действия кеша просмотра в Django?

@cache_page decorator является удивительным. Но для моего блога я хотел бы сохранить страницу в кеше, пока кто-то не прокомментирует сообщение. Это звучит как отличная идея, так как люди редко комментируют, поэтому держать страницы в memcached, пока никто не комментирует, было бы замечательно. Я думаю, что кто-то должен был иметь эту проблему раньше? И это отличается от кэширования на URL.

Итак, решение, о котором я думаю, это:

@cache_page( 60 * 15, "blog" );
def blog( request ) ...

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

17 ответов

Решение

Это решение работает для версий django до 1.7

Вот решение, которое я написал, чтобы сделать то, о чем вы говорите в некоторых из моих собственных проектов:

def expire_view_cache(view_name, args=[], namespace=None, key_prefix=None):
    """
    This function allows you to invalidate any view-level cache. 
        view_name: view function you wish to invalidate or it's named url pattern
        args: any arguments passed to the view function
        namepace: optioal, if an application namespace is needed
        key prefix: for the @cache_page decorator for the function (if any)
    """
    from django.core.urlresolvers import reverse
    from django.http import HttpRequest
    from django.utils.cache import get_cache_key
    from django.core.cache import cache
    # create a fake request object
    request = HttpRequest()
    # Loookup the request path:
    if namespace:
        view_name = namespace + ":" + view_name
    request.path = reverse(view_name, args=args)
    # get cache key, expire if the cached item exists:
    key = get_cache_key(request, key_prefix=key_prefix)
    if key:
        if cache.get(key):
            # Delete the cache entry.  
            #
            # Note that there is a possible race condition here, as another 
            # process / thread may have refreshed the cache between
            # the call to cache.get() above, and the cache.set(key, None) 
            # below.  This may lead to unexpected performance problems under 
            # severe load.
            cache.set(key, None, 0)
        return True
    return False

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

Чтобы использовать его так, как вы говорите, попробуйте что-то вроде:

from django.db.models.signals import post_save
from blog.models import Entry

def invalidate_blog_index(sender, **kwargs):
    expire_view_cache("blog")

post_save.connect(invalidate_portfolio_index, sender=Entry)

Таким образом, в основном, когда объект записи в блоге сохраняется, вызывается invalidate_blog_index и срок действия кэшированного представления истекает. NB: я не проверял это всесторонне, но пока он работал нормально.

Декоратор cache_page в конце будет использовать CacheMiddleware, который сгенерирует ключ кеша на основе запроса (см. django.utils.cache.get_cache_key) и key_prefix ("блог" в вашем случае). Обратите внимание, что "блог" - это всего лишь префикс, а не весь ключ кэша.

Вы можете получить уведомление с помощью сигнала django post_save при сохранении комментария, затем вы можете попытаться создать ключ кэша для соответствующих страниц и, наконец, сказать cache.delete(key),

Однако для этого требуется cache_key, который создается с запросом для ранее кэшированного представления. Этот объект запроса недоступен при сохранении комментария. Вы можете создать ключ кеша без надлежащего объекта запроса, но эта конструкция происходит в функции, помеченной как private (_generate_cache_header_key), поэтому вы не должны использовать эту функцию напрямую. Тем не менее, вы можете создать объект, который имеет атрибут пути, такой же, как для исходного кэшированного представления, и Django не заметит, но я не рекомендую этого.

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

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

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

Я написал Django-groupcache для таких ситуаций (вы можете скачать код здесь). В вашем случае вы могли бы написать:

from groupcache.decorators import cache_tagged_page

@cache_tagged_page("blog", 60 * 15)
def blog(request):
    ...

Оттуда вы можете просто сделать позже:

from groupcache.utils import uncache_from_tag

# Uncache all view responses tagged as "blog"
uncache_from_tag("blog") 

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

С последней версией Django(>=2.0) то, что вы ищете, очень легко реализовать:

from django.utils.cache import learn_cache_key
from django.core.cache import cache

keys = set()

@cache_page( 60 * 15, "blog" );
def blog( request ):
    response = render(request, 'template')
    keys.add(learn_cache_key(request, response)
    return response

def invalidate_cache()
    cache.delete_many(keys)

Вы можете зарегистрировать invalidate_cache в качестве обратного вызова, когда кто-то обновляет сообщение в блоге с помощью сигнала pre_save.

Это не будет работать на Django 1,7; как вы можете видеть здесь https://docs.djangoproject.com/en/dev/releases/1.7/ новые ключи кеша сгенерировано с полным URL, поэтому поддельный запрос только для пути не будет работать. Вы должны правильно настроить запрос значения хоста.

fake_meta = {'HTTP_HOST':'myhost',}
request.META = fake_meta

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

Аннулирование кэша представления Django для v1.7 и выше. Проверено на Django 1.9.

def invalidate_cache(path=''):
    ''' this function uses Django's caching function get_cache_key(). Since 1.7, 
        Django has used more variables from the request object (scheme, host, 
        path, and query string) in order to create the MD5 hashed part of the
        cache_key. Additionally, Django will use your server's timezone and 
        language as properties as well. If internationalization is important to
        your application, you will most likely need to adapt this function to
        handle that appropriately.
    '''
    from django.core.cache import cache
    from django.http import HttpRequest
    from django.utils.cache import get_cache_key

    # Bootstrap request:
    #   request.path should point to the view endpoint you want to invalidate
    #   request.META must include the correct SERVER_NAME and SERVER_PORT as django uses these in order
    #   to build a MD5 hashed value for the cache_key. Similarly, we need to artificially set the 
    #   language code on the request to 'en-us' to match the initial creation of the cache_key. 
    #   YMMV regarding the language code.        
    request = HttpRequest()
    request.META = {'SERVER_NAME':'localhost','SERVER_PORT':8000}
    request.LANGUAGE_CODE = 'en-us'
    request.path = path

    try:
        cache_key = get_cache_key(request)
        if cache_key :
            if cache.has_key(cache_key):
                cache.delete(cache_key)
                return (True, 'successfully invalidated')
            else:
                return (False, 'cache_key does not exist in cache')
        else:
            raise ValueError('failed to create cache_key')
    except (ValueError, Exception) as e:            
        return (False, e)

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

status, message = invalidate_cache(path='/api/v1/blog/')

У меня была та же проблема, и я не хотел связываться с HTTP_HOST, поэтому я создал свой собственный декоратор cache_page:

from django.core.cache import cache


def simple_cache_page(cache_timeout):
    """
    Decorator for views that tries getting the page from the cache and
    populates the cache if the page isn't in the cache yet.

    The cache is keyed by view name and arguments.
    """
    def _dec(func):
        def _new_func(*args, **kwargs):
            key = func.__name__
            if kwargs:
                key += ':' + ':'.join([kwargs[key] for key in kwargs])

            response = cache.get(key)
            if not response:
                response = func(*args, **kwargs)
                cache.set(key, response, cache_timeout)
            return response
        return _new_func
    return _dec

Чтобы просроченный кеш страницы просто нужно вызвать:

cache.set('map_view:' + self.slug, None, 0)

где self.slug - параметр из urls.py

url(r'^map/(?P<slug>.+)$', simple_cache_page(60 * 60 * 24)(map_view), name='map'), 

Джанго 1.11, Питон 3.4.3

Я должен был изменить решение mazelife, чтобы оно заработало:

def expire_view_cache(view_name, args=[], namespace=None, key_prefix=None, method="GET"):
    """
    This function allows you to invalidate any view-level cache. 
        view_name: view function you wish to invalidate or it's named url pattern
        args: any arguments passed to the view function
        namepace: optioal, if an application namespace is needed
        key prefix: for the @cache_page decorator for the function (if any)

        from: http://stackru.com/questions/2268417/expire-a-view-cache-in-django
        added: method to request to get the key generating properly
    """
    from django.core.urlresolvers import reverse
    from django.http import HttpRequest
    from django.utils.cache import get_cache_key
    from django.core.cache import cache
    # create a fake request object
    request = HttpRequest()
    request.method = method
    # Loookup the request path:
    if namespace:
        view_name = namespace + ":" + view_name
    request.path = reverse(view_name, args=args)
    # get cache key, expire if the cached item exists:
    key = get_cache_key(request, key_prefix=key_prefix)
    if key:
        if cache.get(key):
            cache.set(key, None, 0)
        return True
    return False

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

Вместо явного истечения срока действия кэша вы, возможно, могли бы использовать новый "key_prefix" каждый раз, когда кто-то комментирует сообщение. Например, это может быть дата и время последнего комментария к записи (вы можете даже объединить это значение с Last-Modified заголовок).

К сожалению, Джанго (в том числе cache_page()) не поддерживает динамические "key_prefix" es (проверено на Django 1.9), но существует обходной путь. Вы можете реализовать свой собственный cache_page() который может использовать расширенный CacheMiddleware с динамической поддержкой "key_prefix". Например:

from django.middleware.cache import CacheMiddleware
from django.utils.decorators import decorator_from_middleware_with_args

def extended_cache_page(cache_timeout, key_prefix=None, cache=None):
    return decorator_from_middleware_with_args(ExtendedCacheMiddleware)(
        cache_timeout=cache_timeout,
        cache_alias=cache,
        key_prefix=key_prefix,
    )

class ExtendedCacheMiddleware(CacheMiddleware):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if callable(self.key_prefix):
            self.key_function = self.key_prefix

    def key_function(self, request, *args, **kwargs):
        return self.key_prefix

    def get_key_prefix(self, request):
        return self.key_function(
            request,
            *request.resolver_match.args,
            **request.resolver_match.kwargs
        )

    def process_request(self, request):
        self.key_prefix = self.get_key_prefix(request)
        return super().process_request(request)

    def process_response(self, request, response):
        self.key_prefix = self.get_key_prefix(request)
        return super().process_response(request, response)

Тогда в вашем коде:

from django.utils.lru_cache import lru_cache

@lru_cache()
def last_modified(request, blog_id):
    """return fresh key_prefix"""

@extended_cache_page(60 * 15, key_prefix=last_modified)
def view_blog(request, blog_id):
    """view blog page with comments"""

Большинство приведенных выше решений не сработали в нашем случае, потому что мы используем. Исходный код дляget_cache_key показывает, что он использует request.get_absolute_uri() для генерации ключа кеша.

По умолчанию HttpRequest класс устанавливает scheme в виде http. Таким образом, нам нужно переопределить его, чтобы использовать https для нашего фиктивного объекта запроса.

Это код, который нам подходит :)

      from django.core.cache import cache
from django.http import HttpRequest
from django.utils.cache import get_cache_key


class HttpsRequest(HttpRequest):
    @property
    def scheme(self):
        return "https"


def invalidate_cache_page(
    path,
    query_params=None,
    method="GET",
):
    request = HttpsRequest()

    # meta information can be checked from error logs
    request.META = {
        "SERVER_NAME": "www.yourwebsite.com",
        "SERVER_PORT": "443",
        "QUERY_STRING": query_params,
    }

    request.path = path
    key = get_cache_key(request, method=method)
    if cache.has_key(key):
        cache.delete(key)

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

      page = reverse('url_name', kwargs={'id': obj.id})
invalidate_cache_page(path)

Ответ Дункана хорошо работает с Джанго 1.9. Но если нам нужно сделать недействительным URL с GET-параметром, мы должны внести небольшие изменения в запрос. Например, для.../? Mykey=myvalue

request.META = {'SERVER_NAME':'127.0.0.1','SERVER_PORT':8000, 'REQUEST_METHOD':'GET', 'QUERY_STRING': 'mykey=myvalue'}
request.GET.__setitem__(key='mykey', value='myvalue')

Простая вспомогательная функция, которая очищает кеш для данного URL-адреса без заголовков. Вероятно, лучше всего использовать при подключении к событию post_save модели через сигналы. Дополнительную информацию см. в сообщении в списке рассылки пользователей django.

      from django.core.cache import cache
from django.http import HttpRequest
from django.utils.cache import get_cache_key

def expire_page(path):
    request = HttpRequest()
    request.path = path
    key = get_cache_key(request)
    if cache.has_key(key):   
        cache.delete(key)

Я боролся с подобной ситуацией, и вот решение, которое я придумал, я запустил его на более ранней версии Django, но в настоящее время он используется на версии 2.0.3.

Первая проблема: когда вы устанавливаете объекты для кэширования в Django, он устанавливает заголовки так, чтобы последующие кэши, включая кэш браузера, кэшировали вашу страницу.

Чтобы переопределить это, вам нужно установить промежуточное ПО. Я взломал это из других источников на Stackru, но не могу найти его в данный момент. В appname/middleware.py:

from django.utils.cache import add_never_cache_headers


class Disable(object):

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        add_never_cache_headers(response)
        return response

Затем в settings.py, чтобы MIDDLEWARE, добавлять:

'appname.middleware.downstream_caching.Disable',

Имейте в виду, что этот подход полностью отключает последующее кэширование, что может не соответствовать вашим ожиданиям.

Наконец, я добавил в свой views.py:

def expire_page(request, path=None, query_string=None, method='GET'):
    """
    :param request: "real" request, or at least one providing the same scheme, host, and port as what you want to expire
    :param path: The path you want to expire, if not the path on the request
    :param query_string: The query string you want to expire, as opposed to the path on the request
    :param method: the HTTP method for the page, if not GET
    :return: None
    """
    if query_string is not None:
        request.META['QUERY_STRING'] = query_string
    if path is not None:
        request.path = path
    request.method = method

    # get_raw_uri and method show, as of this writing, everything used in the cache key
    # print('req uri: {} method: {}'.format(request.get_raw_uri(), request.method))
    key = get_cache_key(request)
    if key in cache:
        cache.delete(key)

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

Еще одна обновленная версия ответа Дункана: пришлось выяснить правильные мета-поля: (проверено на Django 1.9.8)

def invalidate_cache(path=''):
    import socket
    from django.core.cache import cache
    from django.http import HttpRequest
    from django.utils.cache import get_cache_key

    request = HttpRequest()
    domain = 'www.yourdomain.com'
    request.META = {'SERVER_NAME': socket.gethostname(), 'SERVER_PORT':8000, "HTTP_HOST": domain, 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br'}
    request.LANGUAGE_CODE = 'en-us'
    request.path = path

    try:
        cache_key = get_cache_key(request)
        if cache_key :
            if cache.has_key(cache_key):
                cache.delete(cache_key)
                return (True, 'successfully invalidated')
            else:
                return (False, 'cache_key does not exist in cache')
        else:
            raise ValueError('failed to create cache_key')
    except (ValueError, Exception) as e:            
        return (False, e)

Теперь все просто (проверено на Django 1.10)

from django.db.models.signals import post_save
from django.core.cache import cache
from django.dispatch import receiver

@receiver(post_save)
def clear_the_cache(**kwargs):
    cache.clear()

Решение простое и не требует дополнительной работы.

пример

@cache_page(60 * 10)
def our_team(request, sorting=None):
    ...

Это установит ответ на кеш с ключом по умолчанию.

Срок действия кэша представления

from django.utils.cache import get_cache_key
from django.core.cache import cache

# This will remove the cache value and set it to None
cache.set(get_cache_key(request), None)

Просто, Чисто, Быстро.

Другие вопросы по тегам