我想实现一个装饰器,它为任何方法提供每请求缓存,而不仅仅是视图。这是一个用例示例。
我有一个自定义标签,用于确定是否 一长串记录中的记录是 一个“最爱”。为了检查是否 item是最喜欢的,你必须查询 数据库。理想情况下,你会 执行一个查询以获取所有 收藏夹,然后检查一下 针对每条记录缓存列表。
一个解决方案是获得所有 视图中的收藏夹,然后通过 设置到模板中,然后 进入每个标签呼叫。
或者,标签本身可以 执行查询本身,但只有 第一次叫它。那么 结果可以缓存后续 调用。好处是你可以使用 这个标签来自任何模板 查看,但没有提醒视图。
在现有的缓存机制中,您 可以将结果缓存50ms, 并假设这将与 当前的要求。我想做到这一点 相关性可靠。
以下是我目前拥有的标签示例。
@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
有没有办法从Django获取当前请求对象,没有传递它?从标签,我可以传递请求,它将始终存在。但是我想从其他函数中使用这个装饰器。
是否存在按请求缓存的现有实现?
答案 0 :(得分:24)
使用自定义中间件,您可以获得保证为每个请求清除的Django缓存实例。
这是我在项目中使用的:
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()
要使用中间件,请在settings.py中注册它,例如:
MIDDLEWARE_CLASSES = (
...
'myapp.request_cache.RequestCacheMiddleware'
)
然后您可以按如下方式使用缓存:
from myapp.request_cache import get_request_cache
cache = get_request_cache()
有关更多信息,请参阅django低级别缓存api文档:
修改memoize装饰器以使用请求缓存应该很容易。看一下Python装饰器库,获取memoize装饰器的一个很好的例子:
答案 1 :(得分:3)
我想出了一个用于将事物直接缓存到请求对象中的hack(而不是使用标准缓存,它将绑定到memcached,文件,数据库等)。
# 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
所以,基本上我只是将缓存的值(在这种情况下是类别对象)存储在一个新的密钥' c_category'在请求的字典中。或者更确切地说,因为我们不能在请求对象上创建密钥,所以我将密钥添加到请求对象的一个方法 - get_host()。
格奥尔基。
答案 2 :(得分:3)
多年以后,在单个Django请求中缓存SELECT语句的超级黑客攻击。您需要在请求范围的早期执行patch()
方法,就像在一个中间件中一样。
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
patch()方法用名为execute_sql_cache的替换替换Django内部execute_sql方法。该方法查看要运行的sql,如果是select语句,则首先检查线程本地缓存。只有在缓存中找不到它才会继续执行SQL。在任何其他类型的sql语句中,它会吹掉缓存。有一些逻辑可以不缓存大型结果集,这意味着超过100条记录。这是为了保留Django的懒惰查询集评估。
答案 3 :(得分:3)
编辑:我提出的最终解决方案已编译成PyPI包:https://pypi.org/project/django-request-cache/
这里没有其他解决方案解决的一个主要问题是LocMemCache在单个进程的生命周期中创建和销毁其中一些内存时会泄漏内存。 django.core.cache.backends.locmem
定义了几个全局字典,这些字典包含对每个LocalMemCache实例的缓存数据的引用,并且这些字典永远不会被清空。
以下代码解决了这个问题。它起初是@href _的答案和@ squarelogic.hayden评论中链接的代码使用的清洁逻辑的组合,然后我进一步完善。
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
编辑2016-06-15: 我发现了一个解决这个问题的一个非常简单的解决方案,并且因为没有意识到从一开始就应该有多容易。
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()
通过这种方式,您可以使用request.cache
作为缓存实例,只有request
生效,并且在请求完成后将由垃圾收集器完全清理。
如果您需要从通常不可用的上下文中访问request
对象,您可以使用所谓的&#34;全局请求中间件&#34的各种实现之一;可以在网上找到。
答案 4 :(得分:2)
您可以随时手动执行缓存。
...
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)
...
答案 5 :(得分:1)
这个使用python dict作为缓存(不是django的缓存),并且简单而轻巧。
可以使用threadlocal存储实现相同的功能。 我不知道这种方法的任何缺点,请随意在评论中添加它们。
from threading import currentThread
import weakref
_request_cache = weakref.WeakKeyDictionary()
def get_request_cache():
return _request_cache.setdefault(currentThread(), {})
答案 6 :(得分:1)
Answer很棒。
如果你想要更短的东西,也可以做到这一点:
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)
然后得到喜欢这样称呼:
favourites = cached_call(get_favourites, request.user)
此方法使用lru cache并将其与时间戳结合使用,我们确保缓存不会持续任何时间超过几秒钟。如果你需要在短时间内多次调用昂贵的功能,这就解决了这个问题。
这不是使缓存无效的完美方式,因为它偶尔会错过最近的数据:int(..2.99.. / 3)
后跟int(..3.00..) / 3)
。尽管有这个缺点,它仍然可以在大多数击中时非常有效。
另外作为奖励,您可以在请求/响应周期之外使用它,例如芹菜任务或管理命令作业。