在Django中过期视图缓存?

时间:2010-02-15 19:33:40

标签: python django caching

@cache_page decorator太棒了。但对于我的博客,我想在缓存中保留一个页面,直到有人对帖子发表评论。这听起来像个好主意,因为人们很少发表评论,因此将页面保留在memcached中,而没有人评论会很好。我以为某人之前一定有过这个问题?这与每个网址的缓存不同。

我正在考虑的解决方案是:

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

然后我会保留用于博客视图的所有缓存密钥列表,然后让“博客”缓存空间到期。但是我对Django没有超级经验,所以我想知道是否有人知道更好的方法吗?

16 个答案:

答案 0 :(得分:44)

此解决方案适用于1.7 之前的django版本

这是我写的一个解决方案,用于处理我自己的一些项目:

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)

所以基本上,当保存博客Entry对象时,会调用invalidate_blog_index并且缓存的视图已过期。注意:没有对此进行过广泛的测试,但到目前为止它对我来说还算不错。

答案 1 :(得分:11)

我为这种情况写了Django-groupcache(你可以download the code here)。在你的情况下,你可以写:

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():它稍微涉及一些,但它允许您根据模型实体更改自动解除响应。

答案 2 :(得分:9)

使用最新版本的Django(> = 2.0),您正在寻找的内容非常容易实现:

from django.utils.cache import learn_cache_key
from django.core.cache import cache
from django.views.decorators.cache import cache_page

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)

当有人通过pre_save信号更新博客中的帖子时,您可以将invalidate_cache注册为回调。

答案 3 :(得分:6)

cache_page装饰器最后将使用CacheMiddleware,它将根据请求(查看django.utils.cache.get_cache_key)和key_prefix(在您的情况下为“blog”)生成缓存键。请注意,“blog”只是一个前缀,而不是整个缓存键。

保存评论后,您可以通过django's post_save signal收到通知,然后您可以尝试为相应的页面构建缓存密钥,最后说cache.delete(key)

然而,这需要cache_key,它是使用先前缓存的视图的请求构造的。保存注释时,此请求对象不可用。您可以在没有正确请求对象的情况下构造缓存键,但是此构造发生在标记为私有(_generate_cache_header_key)的函数中,因此您不应该直接使用此函数。但是,您可以构建一个对象,其路径属性与原始缓存视图相同,Django不会注意到,但我不建议这样做。

cache_page装饰器为您提取了相当多的缓存,并且很难直接删除某个缓存对象。你可以用相同的方式组成自己的键并处理它们,但这需要更多的编程,而不像cache_page装饰器那样抽象。

当您的评论显示在多个视图中时,您还必须删除多个缓存对象(即带有评论计数的索引页面和各个博客条目页面)。

总结一下:Django为你做了基于时间的缓存密钥到期,但是在适当的时候自定义删除缓存密钥更加棘手。

答案 4 :(得分:5)

这对django 1.7无效;正如您在此处看到的https://docs.djangoproject.com/en/dev/releases/1.7/#cache-keys-are-now-generated-from-the-request-s-absolute-url,新的缓存密钥是使用完整的URL生成的,因此仅路径的虚假请求将不起作用。您必须正确设置请求主机值。

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

如果您有多个域使用相同的视图,您应该在HTTP_HOST中循环它们,获取正确的密钥并为每个域进行清理。

答案 5 :(得分:5)

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/')

答案 6 :(得分:3)

如果没有评论,您可以手动缓存博客文章对象(或类似文件),而不是使用缓存页面装饰器,然后当有第一条评论时,重新缓存博客帖子对象,以便它是最新的(假设该对象具有引用任何注释的属性),但只是让评论的博客帖子的缓存数据到期,然后再没有麻烦重新缓存......

答案 7 :(得分:3)

FWIW我不得不修改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://stackoverflow.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

答案 8 :(得分:3)

我遇到了同样的问题而且我不想弄乱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

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

Django 1.11,Python 3.4.3

答案 9 :(得分:0)

每次有人评论帖子时,您都可以使用新的“key_prefix”,而不是显式缓存过期。例如。它可能是最后一篇帖子评论的日期时间(你甚至可以将这个值与Last-Modified标题组合起来。)

不幸的是,Django(包括cache_page())不支持动态“key_prefix”es(检查 Django 1.9 ),但存在解决方法。您可以实现自己的cache_page(),其中可能使用包含动态“key_prefix”支持的扩展CacheMiddleware。例如:

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"""

答案 10 :(得分:0)

Duncan的答案适用于Django 1.9。但是如果我们需要使用GET参数的无效url,我们必须对请求进行一些更改。 例如,对于... /?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')

答案 11 :(得分:0)

我在类似的情况下挣扎,这是我提出的解决方案,我在早期版本的Django上启动它,但它目前在2.0.3版本上使用。

第一个问题:当您在Django中设置要缓存的内容时,它会设置标头,以便下游缓存(包括浏览器缓存)缓存您的页面。

要覆盖它,您需要设置中间件。我在StackOverflow的其他地方抄袭了这个,但目前还无法找到它。在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.pyMIDDLEWARE,添加:

'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对象,但在撰写本文时,它提供了请求的方案/协议,主机和端口,几乎所有请求对象都是站点/应用程序将执行,只要您传入路径和查询字符串。

答案 12 :(得分:0)

Duncan的答案的另一个更新版本:必须弄清楚正确的元字段:(在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)

答案 13 :(得分:0)

上述大多数解决方案在我们的案例中都不起作用,因为我们使用了 httpsget_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)

答案 14 :(得分:-1)

解决方案很简单,不需要任何额外的工作。

实施例

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

简单,干净,快速。

答案 15 :(得分:-1)

现在更简单了(已在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()