framework - ¿Caché por solicitud en Django?
django using cache (7)
Me gustaría implementar un decorador que proporcione almacenamiento en caché por solicitud a cualquier método, no solo a las vistas. Aquí hay un ejemplo de un caso de uso.
Tengo una etiqueta personalizada que determina si un registro en una larga lista de registros es un "favorito". Para verificar si un elemento es un favorito, debe consultar la base de datos. Lo ideal sería realizar una consulta para obtener todos los favoritos y, a continuación, simplemente verificar esa lista en caché con cada registro.
Una solución es obtener todos los favoritos en la vista, y luego pasar ese conjunto a la plantilla y luego a cada llamada de etiqueta.
Alternativamente, la etiqueta en sí podría realizar la consulta en sí misma, pero solo la primera vez que se llama. Entonces los resultados podrían ser almacenados en caché para llamadas posteriores. La ventaja es que puede usar esta etiqueta desde cualquier plantilla, en cualquier vista, sin alertar a la vista.
En el mecanismo de almacenamiento en caché existente, podría simplemente almacenar en caché el resultado durante 50 ms y asumir que se correlacionaría con la solicitud actual. Quiero hacer esa correlación confiable.
Aquí hay un ejemplo de la etiqueta que tengo actualmente.
@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
¿Hay una manera de obtener el objeto de solicitud actual de Django, sin pasarlo? Desde una etiqueta, podría pasar la solicitud, que siempre existirá. Pero me gustaría usar este decorador de otras funciones.
¿Hay una implementación existente de un caché por solicitud?
Años más tarde, un súper pirateo para almacenar en caché sentencias SELECT dentro de una sola solicitud de Django. Debe ejecutar el método patch()
desde el principio en el ámbito de su solicitud, como en una pieza de middleware.
from threading import local
import itertools
from django.db.models.sql.constants import MULTI
from django.db.models.sql.compiler import SQLCompiler
from django.db.models.sql.datastructures import EmptyResultSet
from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE
_thread_locals = local()
def get_sql(compiler):
'''''' get a tuple of the SQL query and the arguments ''''''
try:
return compiler.as_sql()
except EmptyResultSet:
pass
return ('''', [])
def execute_sql_cache(self, result_type=MULTI):
if hasattr(_thread_locals, ''query_cache''):
sql = get_sql(self) # (''SELECT * FROM ...'', (50)) <= sql string, args tuple
if sql[0][:6].upper() == ''SELECT'':
# uses the tuple of sql + args as the cache key
if sql in _thread_locals.query_cache:
return _thread_locals.query_cache[sql]
result = self._execute_sql(result_type)
if hasattr(result, ''next''):
# only cache if this is not a full first page of a chunked set
peek = result.next()
result = list(itertools.chain([peek], result))
if len(peek) == GET_ITERATOR_CHUNK_SIZE:
return result
_thread_locals.query_cache[sql] = result
return result
else:
# the database has been updated; throw away the cache
_thread_locals.query_cache = {}
return self._execute_sql(result_type)
def patch():
'''''' patch the django query runner to use our own method to execute sql ''''''
_thread_locals.query_cache = {}
if not hasattr(SQLCompiler, ''_execute_sql''):
SQLCompiler._execute_sql = SQLCompiler.execute_sql
SQLCompiler.execute_sql = execute_sql_cache
El método patch () reemplaza el método interno execute_sql de Django con un stand-in llamado execute_sql_cache. Ese método examina el sql que se ejecutará, y si se trata de una declaración de selección, primero verifica un caché de subproceso. Solo si no se encuentra en la memoria caché se procede a ejecutar el SQL. En cualquier otro tipo de declaración SQL, elimina la memoria caché. Existe cierta lógica para no almacenar en caché grandes conjuntos de resultados, es decir, cualquier cosa que supere los 100 registros. Esto es para preservar la perezosa evaluación de conjuntos de consultas de Django.
EDITAR: La solución final que se me ocurrió se ha compilado en un paquete PyPI: https://pypi.org/project/django-request-cache/
Un problema importante que ninguna otra solución aquí resuelve es el hecho de que LocMemCache pierde memoria cuando crea y destruye varios de ellos durante la vida de un solo proceso. django.core.cache.backends.locmem
define varios diccionarios globales que contienen referencias a los datos de caché de cada instancia de LocalMemCache, y esos diccionarios nunca se vacían.
El siguiente código resuelve este problema. Comenzó como una combinación de la respuesta de @ href_ y la lógica más limpia utilizada por el código vinculado en el comentario de @ squarelogic.hayden, que luego refiné más.
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
EDICIÓN 2016-06-15: Descubrí una solución significativamente más simple para este problema, y algo así como no me di cuenta de lo fácil que debería haber sido desde el principio.
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 esto, puede usar request.cache
como una instancia de caché que vive solo mientras la request
haga, y el recolector de basura lo eliminará por completo cuando se complete la solicitud.
Si necesita acceder al objeto de request
desde un contexto donde normalmente no está disponible, puede usar una de las diversas implementaciones del denominado "middleware de solicitud global" que se puede encontrar en línea.
Este utiliza un dict de python como el caché (no el caché de django), y es muy simple y ligero.
- Cada vez que se destruye el hilo, su caché será demasiado automática.
- No requiere ningún middleware, y el contenido no es decapado y despachado en cada acceso, que es más rápido.
- Probado y funciona con la crianza de los monos de gevent.
Lo mismo puede implementarse con el almacenamiento threadlocal. No tengo conocimiento de ninguna desventaja de este enfoque, siéntase libre de agregarlos en los comentarios.
from threading import currentThread
import weakref
_request_cache = weakref.WeakKeyDictionary()
def get_request_cache():
return _request_cache.setdefault(currentThread(), {})
Se me ocurrió un truco para almacenar cosas directamente en el objeto de solicitud (en lugar de usar el caché estándar, que estará vinculado a memcached, archivo, base de datos, etc.)
# 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
Entonces, básicamente, solo estoy almacenando el valor almacenado en caché (objeto de categoría en este caso) bajo una nueva clave ''c_category'' en el diccionario de la solicitud. O para ser más precisos, porque no podemos simplemente crear una clave en el objeto de solicitud, estoy agregando la clave a uno de los métodos del objeto de solicitud: get_host ().
Georgy.
Siempre se puede hacer el almacenamiento en caché manualmente.
...
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)
...
Usando un middleware personalizado, puede obtener una instancia de caché de Django que se garantiza que se borrará para cada solicitud.
Esto es lo que usé en un proyecto:
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 = ''locmemcache@%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()
Para usar el middleware, regístrelo en settings.py, por ejemplo:
MIDDLEWARE_CLASSES = (
...
''myapp.request_cache.RequestCacheMiddleware''
)
A continuación, puede utilizar el caché de la siguiente manera:
from myapp.request_cache import get_request_cache
cache = get_request_cache()
Consulte la api doc de caché de bajo nivel de django para obtener más información:
API de caché de bajo nivel Django
Debería ser fácil modificar un decorador memoize para usar el caché de solicitud. Eche un vistazo a la biblioteca de Python Decorator para ver un buen ejemplo de un decorador memoize:
Answer dada por @href_ es genial.
En caso de que quieras algo más corto que también podría hacer el truco:
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)
A continuación, obtener favoritos llamándolo así:
favourites = cached_call(get_favourites, request.user)
Este método utiliza el caché lru y, al combinarlo con la marca de tiempo, nos aseguramos de que el caché no contenga nada durante más de unos pocos segundos. Si necesita llamar a la función costosa varias veces en un corto período de tiempo, esto resuelve el problema.
No es una forma perfecta de invalidar el caché, porque ocasionalmente faltará en datos muy recientes: int(..2.99.. / 3)
seguido de int(..3.00..) / 3)
. A pesar de este inconveniente, todavía puede ser muy efectivo en la mayoría de los hits.
También como un bono, puede usarlo fuera de los ciclos de solicitud / respuesta, por ejemplo, tareas de apio o tareas de comando de gestión.