2010-06-30 4 views
14

Vorrei implementare un decoratore che fornisca il caching per richiesta su qualsiasi metodo, non solo sulle viste. Ecco un esempio di utilizzo.cache per richiesta in Django?

Ho un tag personalizzato che determina se un record in una lunga lista di record è un "preferito". Per verificare se un articolo è un preferito, è necessario interrogare il database . Idealmente, è necessario eseguire una query per ottenere tutti i preferiti, quindi controllare l'elenco memorizzato per ogni record.

Una soluzione è quella di ottenere tutti i favoriti nella vista e quindi passare che impostare nel modello, e poi in ogni chiamata tag.

In alternativa, lo stesso tag potrebbe eseguire la query stessa, ma solo la prima volta che viene chiamato. Quindi i risultati potrebbero essere memorizzati nella cache per le successive chiamate . Il lato positivo è che puoi usare questo tag da qualsiasi modello, su qualsiasi vista, senza avvisare la vista.

Nel meccanismo di caching esistente, è potrebbe semplicemente memorizzare nella cache il risultato per 50 ms, e assumere che sarebbe correlare alla richiesta corrente . Voglio rendere affidabile la correlazione .

Ecco un esempio del tag che attualmente ho.

@register.filter() 
def is_favorite(record, request): 

    if "get_favorites" in request.POST: 
     favorites = request.POST["get_favorites"] 
    else: 

     favorites = get_favorites(request.user) 

     post = request.POST.copy() 
     post["get_favorites"] = favorites 
     request.POST = post 

    return record in favorites 

C'è un modo per ottenere l'oggetto di richiesta corrente da Django, senza passarlo? Da un tag, potevo semplicemente passare la richiesta, che esisterà sempre. Ma mi piacerebbe usare questo decoratore da altre funzioni.

Esiste un'implementazione esistente di una cache per richiesta?

risposta

21

Utilizzando un middleware personalizzato è possibile ottenere un'istanza della cache Django garantita per ogni richiesta.

Questo è quello che ho usato in un progetto:

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

_request_cache = {} 
_installed_middleware = False 

def get_request_cache(): 
    assert _installed_middleware, 'RequestCacheMiddleware not loaded' 
    return _request_cache[currentThread()] 

# LocMemCache is a threadsafe local memory cache 
class RequestCache(LocMemCache): 
    def __init__(self): 
     name = '[email protected]%i' % hash(currentThread()) 
     params = dict() 
     super(RequestCache, self).__init__(name, params) 

class RequestCacheMiddleware(object): 
    def __init__(self): 
     global _installed_middleware 
     _installed_middleware = True 

    def process_request(self, request): 
     cache = _request_cache.get(currentThread()) or RequestCache() 
     _request_cache[currentThread()] = cache 

     cache.clear() 

Per utilizzare il middleware registrarlo in settings.py, ad esempio:

MIDDLEWARE_CLASSES = (
    ... 
    'myapp.request_cache.RequestCacheMiddleware' 
) 

Si può quindi usare la cache come segue:

from myapp.request_cache import get_request_cache 

cache = get_request_cache() 

Per ulteriori informazioni, fare riferimento al documento di cache a basso livello django:

Django Low-Level Cache API

Dovrebbe essere facile modificare un decoratore Memoize di utilizzare la cache richiesta.Dai un'occhiata al Decorator libreria Python per un buon esempio di un decoratore Memoize:

Python Decorator Library

+3

Diffida di questa soluzione! Il dizionario _request_cache si riempirà continuamente con l'apertura di un numero sempre maggiore di thread per servire gli utenti e non verrà mai ripulito. A seconda di come il tuo server web memorizza le variabili globali di Python, ciò potrebbe causare una perdita di memoria. – CoreDumpError

+1

Si dovrebbe cancellare la cache nel metodo process_response ????? –

+1

Sì - cancella la cache su process_response e process_expception - c'è un ottimo esempio di questo nel plugin middleware django cuser. Vedi: https://github.com/Alir3z4/django-cuser/blob/master/cuser/middleware.py –

1

È sempre possibile eseguire manualmente la memorizzazione nella cache.

... 
    if "get_favorites" in request.POST: 
     favorites = request.POST["get_favorites"] 
    else: 
     from django.core.cache import cache 

     favorites = cache.get(request.user.username) 
     if not favorites: 
      favorites = get_favorites(request.user) 
      cache.set(request.user.username, favorites, seconds) 
    ... 
+0

La domanda riguardava una cache per richiesta. Questa soluzione userebbe la cache anche per la seconda richiesta dell'utente. Ma penso che la maggior parte delle volte cache.get() e cache.set() o meglio. – guettli

3

mi si avvicinò con un hack per le cose memorizzazione nella cache direttamente nel l'oggetto della richiesta (invece di utilizzare la cache di serie, che sarà essere legato a memcached, file di database, ecc)

# get the request object's dictionary (rather one of its methods' dictionary) 
mycache = request.get_host.__dict__ 

# check whether we already have our value cached and return it 
if mycache.get('c_category', False): 
    return mycache['c_category'] 
else: 
    # get some object from the database (a category object in this case) 
    c = Category.objects.get(id = cid) 

    # cache the database object into a new key in the request object 
    mycache['c_category'] = c 

    return c 

Quindi, in pratica sto solo la memorizzazione del valore memorizzato nella cache (categoria oggetto in questo caso) in una nuova chiave 'c_category' nel dizionario della richiesta. O per essere più precisi, perché non possiamo semplicemente creare una chiave sull'oggetto richiesta, sto aggiungendo la chiave a uno dei metodi dell'oggetto richiesta - get_host().

Georgy.

3

Anni dopo, un super hacking per memorizzare le istruzioni SELECT all'interno di una singola richiesta Django. È necessario eseguire il metodo sin dalle prime fasi nell'ambito della richiesta, come in un middleware.

Il metodo patch() sostituisce il metodo execute_sql interna Django con uno stand denominato execute_sql_cache. Questo metodo esamina lo sql da eseguire e, se si tratta di un'istruzione select, controlla prima la cache locale del thread. Solo se non viene trovato nella cache, procede con l'esecuzione dell'SQL. Su qualsiasi altro tipo di istruzione sql, spazza via la cache. C'è una logica per non memorizzare nella cache serie di risultati di grandi dimensioni, vale a dire qualcosa oltre 100 record. Questo è per preservare la valutazione del set di query pigro di Django.

+0

A prima vista, questo sembra interessante. Ma guardandolo si invalida la cache quando non si ottiene un'istruzione SELECT. Sembra ragionevole, finché non hai più processi. Non invaliderà tutte le cache. Una piccola modifica sarebbe quella di memorizzarlo sull'oggetto richiesta in modo che venga ripristinato tra le richieste. – dalore

1

Questo utilizza un dt pitone come cache (non la cache del django) ed è semplice e leggero.

  • Ogni volta che il thread viene distrutto, la cache sarà troppo automaticamente.
  • Non richiede alcun middleware e il contenuto non è decapitato e deployato su ogni accesso, che è più veloce.
  • Testato e funziona con monkeypatching di gevent.

Lo stesso può essere probabilmente implementato con l'archiviazione threadlocal. Non sono a conoscenza di alcun aspetto negativo di questo approccio, sentitevi liberi di aggiungerli nei commenti.

from threading import currentThread 
import weakref 

_request_cache = weakref.WeakKeyDictionary() 

def get_request_cache(): 
    return _request_cache.setdefault(currentThread(), {}) 
+1

La domanda originale riguardava la cache per richiesta e non quella per thread. Nel server basato su pool di thread, l'implementazione non scadrà mai e causerà un esaurimento della memoria. –

1

Answer dato da @href_ è ottimo.

Solo nel caso in cui si desidera qualcosa di più breve che potrebbe anche potenzialmente fare il trucco:

from django.utils.lru_cache import lru_cache 

def cached_call(func, *args, **kwargs): 
    """Very basic temporary cache, will cache results 
    for average of 1.5 sec and no more then 3 sec""" 
    return _cached_call(int(time.time()/3), func, *args, **kwargs) 


@lru_cache(maxsize=100) 
def _cached_call(time, func, *args, **kwargs): 
    return func(*args, **kwargs) 

quindi ottenere preferiti chiamare in questo modo:

favourites = cached_call(get_favourites, request.user) 

Questo metodo si avvale di lru cache e combinandolo con il timestamp ci assicuriamo che la cache non registri nulla per più di qualche secondo. Se è necessario chiamare la funzione costosa più volte in un breve periodo di tempo, questo risolve il problema.

Non è un modo perfetto per invalidare la cache, perché occasionalmente mancherà sui dati più recenti: int(..2.99../3) seguito da int(..3.00..)/3). Nonostante questo inconveniente, può comunque essere molto efficace nella maggior parte dei casi.

Inoltre come bonus è possibile utilizzarlo al di fuori dei cicli di richiesta/risposta, ad esempio attività di sedici o lavori di comando.

2

Un problema importante che nessuna altra soluzione risolve qui è il fatto che LocMemCache perde memoria quando si creano e si distruggono molti di loro nel corso della vita di un singolo processo. django.core.cache.backends.locmem definisce diversi dizionari globali che contengono riferimenti a tutti i dati della cache dell'istanza di LocalMemCache e quei dizionari non vengono mai svuotati.

Il seguente codice risolve questo problema. È iniziato come una combinazione della risposta di @ href_ e della logica più pulita utilizzata dal codice collegato nel commento di @ squarelogic.hayden, che poi ho perfezionato ulteriormente.

from uuid import uuid4 
from threading import current_thread 

from django.core.cache.backends.base import BaseCache 
from django.core.cache.backends.locmem import LocMemCache 
from django.utils.synch import RWLock 


# Global in-memory store of cache data. Keyed by name, to provides multiple 
# named local memory caches. 
_caches = {} 
_expire_info = {} 
_locks = {} 


class RequestCache(LocMemCache): 
    """ 
    RequestCache is a customized LocMemCache with a destructor, ensuring that creating 
    and destroying RequestCache objects over and over doesn't leak memory. 
    """ 

    def __init__(self): 
     # We explicitly do not call super() here, because while we want 
     # BaseCache.__init__() to run, we *don't* want LocMemCache.__init__() to run. 
     BaseCache.__init__(self, {}) 

     # Use a name that is guaranteed to be unique for each RequestCache instance. 
     # This ensures that it will always be safe to call del _caches[self.name] in 
     # the destructor, even when multiple threads are doing so at the same time. 
     self.name = uuid4() 
     self._cache = _caches.setdefault(self.name, {}) 
     self._expire_info = _expire_info.setdefault(self.name, {}) 
     self._lock = _locks.setdefault(self.name, RWLock()) 

    def __del__(self): 
     del _caches[self.name] 
     del _expire_info[self.name] 
     del _locks[self.name] 


class RequestCacheMiddleware(object): 
    """ 
    Creates a cache instance that persists only for the duration of the current request. 
    """ 

    _request_caches = {} 

    def process_request(self, request): 
     # The RequestCache object is keyed on the current thread because each request is 
     # processed on a single thread, allowing us to retrieve the correct RequestCache 
     # object in the other functions. 
     self._request_caches[current_thread()] = RequestCache() 

    def process_response(self, request, response): 
     self.delete_cache() 
     return response 

    def process_exception(self, request, exception): 
     self.delete_cache() 

    @classmethod 
    def get_cache(cls): 
     """ 
     Retrieve the current request's cache. 

     Returns None if RequestCacheMiddleware is not currently installed via 
     MIDDLEWARE_CLASSES, or if there is no active request. 
     """ 
     return cls._request_caches.get(current_thread()) 

    @classmethod 
    def clear_cache(cls): 
     """ 
     Clear the current request's cache. 
     """ 
     cache = cls.get_cache() 
     if cache: 
      cache.clear() 

    @classmethod 
    def delete_cache(cls): 
     """ 
     Delete the current request's cache object to avoid leaking memory. 
     """ 
     cache = cls._request_caches.pop(current_thread(), None) 
     del cache 

EDIT 2016/06/15: ho scoperto una soluzione notevolmente più semplice a questo problema, e kindof facepalmed per non rendersi conto di quanto sia facile questo avrebbe dovuto essere fin dall'inizio.

from django.core.cache.backends.base import BaseCache 
from django.core.cache.backends.locmem import LocMemCache 
from django.utils.synch import RWLock 


class RequestCache(LocMemCache): 
    """ 
    RequestCache is a customized LocMemCache which stores its data cache as an instance attribute, rather than 
    a global. It's designed to live only as long as the request object that RequestCacheMiddleware attaches it to. 
    """ 

    def __init__(self): 
     # We explicitly do not call super() here, because while we want BaseCache.__init__() to run, we *don't* 
     # want LocMemCache.__init__() to run, because that would store our caches in its globals. 
     BaseCache.__init__(self, {}) 

     self._cache = {} 
     self._expire_info = {} 
     self._lock = RWLock() 

class RequestCacheMiddleware(object): 
    """ 
    Creates a fresh cache instance as request.cache. The cache instance lives only as long as request does. 
    """ 

    def process_request(self, request): 
     request.cache = RequestCache() 

Con questo, è possibile utilizzare request.cache come istanza di cache che vive solo fino a quando il request fa, e sarà completamente ripulito dal garbage collector quando la richiesta è fatto.

Se è necessario accedere all'oggetto request da un contesto in cui non è normalmente disponibile, è possibile utilizzare una delle varie implementazioni di un cosiddetto "middleware di richiesta globale" che può essere trovato online.