簡體   English   中英

使 Django 中的視圖緩存過期?

[英]Expire a view-cache in Django?

@cache_page decorator很棒。 但是對於我的博客,我想在緩存中保留一個頁面,直到有人對帖子發表評論。 這聽起來是個好主意,因為人們很少發表評論,因此將頁面保存在 memcached 中而沒有人發表評論會很棒。 我在想之前一定有人遇到過這個問題嗎? 這與按 url 緩存不同。

所以我想到的一個解決方案是:

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

然后我會保留用於博客視圖的所有緩存鍵的列表,然后讓“博客”緩存空間過期。 但是我對 Django 並不是非常有經驗,所以我想知道是否有人知道這樣做的更好方法?

此解決方案適用於 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)

所以基本上,當一個 blog Entry 對象被保存時, invalidate_blog_index 被調用並且緩存的視圖過期。 注意:尚未對此進行廣泛測試,但到目前為止對我來說效果很好。

我為這種情況編寫了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() :它稍微復雜一點,但它允許您根據模型實體更改自動取消緩存響應。

cache_page 裝飾器最終將使用 CacheMiddleware,它將根據請求(查看django.utils.cache.get_cache_key )和 key_prefix (在您的情況下為“博客”)生成一個緩存鍵。 請注意,“blog”只是一個前綴,而不是整個緩存鍵。

您可以在保存評論時通過django 的 post_save 信號獲得通知,然后您可以嘗試為適當的頁面構建緩存鍵,最后說cache.delete(key)

然而,這需要 cache_key,它是根據對先前緩存的視圖的請求構造的。 保存評論時,此請求對象不可用。 您可以在沒有正確請求對象的情況下構造緩存鍵,但這種構造發生在標記為私有 ( _generate_cache_header_key ) 的函數中,因此您不應直接使用此函數。 但是,您可以構建一個具有與原始緩存視圖相同的路徑屬性的對象,Django 不會注意到,但我不建議這樣做。

cache_page 裝飾器為您抽象了緩存,並且很難直接刪除某個緩存對象。 您可以創建自己的鍵並以相同的方式處理它們,但這需要更多的編程並且不像cache_page裝飾器那樣抽象。

當您的評論顯示在多個視圖中時,您還必須刪除多個緩存對象(即帶有評論計數的索引頁面和單個博客條目頁面)。

總結一下:Django 為你做基於時間的緩存鍵過期,但是在正確的時間自定義刪除緩存鍵更棘手。

使用最新版本的 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 注冊為回調。

這不適用於 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 中循環它們,獲取正確的密鑰並對每個域進行清理。

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

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

如果沒有評論,您可以手動緩存博客文章對象(或類似的),而不是使用緩存頁面裝飾器,然后當有第一條評論時,重新緩存博客文章對象,以便它是最新的(假設對象具有引用任何評論的屬性),但是只需讓評論博客文章的緩存數據過期,然后就不用再重新緩存了...

我有同樣的問題,我不想弄亂 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

每次有人評論帖子時,您都可以使用新的“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"""

鄧肯的回答適用於 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')

我遇到了類似的情況,這是我想出的解決方案,我在早期版本的 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對象,但在撰寫本文時,它提供了request的方案/協議、主機和端口,您的站點/應用程序的幾乎任何請求對象都可以,只要您傳入路徑和查詢字符串。

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)

上面的大多數解決方案在我們的案例中都不起作用,因為我們使用https 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)

它現在更簡單(在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

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

簡單,干凈,快速。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM