【问题标题】:Expire a view-cache in Django?使 Django 中的视图缓存过期?
【发布时间】:2011-01-17 03:02:15
【问题描述】:

@cache_page decorator 很棒。但是对于我的博客,我想将一个页面保留在缓存中,直到有人在帖子上出现。这听起来是个好主意,因为人们很少发表评论,因此将页面保留在 memcached 中而没有人使用 cmets 会很棒。我在想以前一定有人遇到过这个问题吗?这与每个 url 缓存不同。

所以我想到的解决方案是:

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

然后我会保留用于博客视图的所有缓存键的列表,然后有办法使“博客”缓存空间过期。但我对 Django 不是很有经验,所以我想知道是否有人知道更好的方法?

【问题讨论】:

    标签: python django caching


    【解决方案1】:

    此解决方案适用于 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)
    

    因此,基本上,每当保存博客条目对象时,都会调用 invalidate_blog_index 并且缓存视图已过期。注意:尚未对此进行广泛测试,但到目前为止对我来说效果很好。

    【讨论】:

    • 它在数据库缓存方面做得很好,但对文件系统缓存不起作用:key = get_cache_key(request) 在文件系统缓存中返回 None,但在数据库缓存中返回正确的键。你知道如何让它与文件系统缓存后端一起运行吗?
    • get_cache_key 什么都不返回。
    • 取消缓存后,下次django会自动cache_page吗?它似乎至少不适用于 redis 缓存后端。
    • 我认为参数列表中的 args=[] 很危险,不是吗?试试这个def my_func(args=[]): args.append('a') return args for i in xrange(5): print my_func()
    • 尝试但对我不起作用。这行:key = get_cache_key(request, key_prefix=key_prefix) return key = None,意思是request在缓存中没有找到,但实际上在缓存中。
    【解决方案2】:

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

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

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

    cache_page 装饰器为您抽象了很多缓存,使直接删除某个缓存对象变得困难。您可以制作自己的密钥并以相同的方式处理它们,但这需要更多的编程,并且不像 cache_page 装饰器那样抽象。

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

    总结一下:Django 会为您执行基于时间的缓存键过期,但在正确的时间自定义删除缓存键更加棘手。

    【讨论】:

      【解决方案3】:

      我为这种情况写了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():它稍微复杂一些,但它允许您根据模型实体更改自动取消缓存响应。

      【讨论】:

      • @laike9m 最新提交是在 2013 年。我会说它现在是一个死项目,但这并不意味着它仍然不能与最新版本的 Django 一起使用。
      • 好吧,存储库无法访问。我想我们可以假设它已经死了。
      【解决方案4】:

      使用最新版本的 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 注册为回调。

      【讨论】:

      • 其实这个不能正常工作,learn_cache_key在代码和缓存中间件中自己生成的时候会返回不同的key..
      • 另外:在cache.delete_many(keys) 之前和之后执行的print(keys) 给出相同的输出。可能还需要其他东西,比如某种.save()
      • keys 是线程本地的。当应用程序在多个线程/进程中运行时,这将不起作用——因为它可能会在生产环境中运行。
      • 嘿,这不会在我们有多个进程的 Gunicorn 或 UWSGI 的情况下产生问题吗?每个进程启动一个新程序,因此有自己的keys。但是,请求可能会到达不同的进程?
      • 即使我的 redis 键是空的,我仍然可以从磁盘获取缓存数据
      【解决方案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 中循环它们,获取正确的密钥并为每个域进行清理。

      【讨论】:

      • Appart 从 HTTP_HOST 你也需要 SERVER_PORT,但我无法用这种方法(在开发环境中)检索正确的密钥:fake_meta = {'HTTP_HOST':'127.0.0.1' , 'SERVER_PORT': 8000} request.META = fake_meta
      【解决方案6】:

      v1.7 及更高版本的 Django 视图缓存失效。在 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/')

      【讨论】:

        【解决方案7】:

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

          我也遇到了同样的问题,我不想弄乱 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'), 
          

          Django 1.11,Python 3.4.3

          【讨论】:

            【解决方案9】:

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

            【讨论】:

            • 我认为缓存整个响应可能是一个想法。调查那个。谢谢。
            【解决方案10】:

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

            不幸的是 Django(包括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"""
            

            【讨论】:

              【解决方案11】:

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

              【讨论】:

                【解决方案12】:

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

                【讨论】:

                  【解决方案13】:

                  我遇到过类似的情况,这是我想出的解决方案,我在早期版本的 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.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 对象,但在撰写本文时,它为请求提供了方案/协议、主机和端口,您的站点/应用程序的几乎所有请求对象都会做,只要你传入路径和查询字符串。

                  【讨论】:

                    【解决方案14】:

                    邓肯回答的另一个更新版本:必须找出正确的元字段:(在 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)
                    

                    【讨论】:

                      【解决方案15】:

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

                      示例

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

                      这将使用默认键设置对缓存的响应。

                      使视图缓存过期

                      from django.utils.cache import get_cache_key
                      from django.core.cache import cache
                      
                      def our_team(request, sorting=None):
                          # This will remove the cache value and set it to None
                          cache.set(get_cache_key(request), None)
                      

                      简单、干净、快速。

                      【讨论】:

                      • request 来自哪里?
                      猜你喜欢
                      • 2012-05-14
                      • 1970-01-01
                      • 2012-10-12
                      • 2013-08-18
                      • 2015-10-20
                      • 2010-10-19
                      • 2012-04-24
                      • 2012-03-10
                      • 2011-09-26
                      相关资源
                      最近更新 更多