Django中的每请求缓存?

时间:2010-06-30 16:48:30

标签: django django-cache

我想实现一个装饰器,它为任何方法提供每请求缓存,而不仅仅是视图。这是一个用例示例。

  

我有一个自定义标签,用于确定是否   一长串记录中的记录是   一个“最爱”。为了检查是否   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获取当前请求对象,没有传递它?从标签,我可以传递请求,它将始终存在。但是我想从其他函数中使用这个装饰器。

是否存在按请求缓存的现有实现?

7 个答案:

答案 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文档:

Django Low-Level Cache API

修改memoize装饰器以使用请求缓存应该很容易。看一下Python装饰器库,获取memoize装饰器的一个很好的例子:

Python Decorator Library

答案 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的缓存),并且简单而轻巧。

  • 每当线程被销毁时,它的缓存就会过于自动。
  • 不需要任何中间件,并且每次访问都不会对内容进行pickle和depickled,这样会更快。
  • 经过测试并使用gevent的monkeypatching。

可以使用threadlocal存储实现相同的功能。 我不知道这种方法的任何缺点,请随意在评论中添加它们。

from threading import currentThread
import weakref

_request_cache = weakref.WeakKeyDictionary()

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

答案 6 :(得分:1)

@href_给出的

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)。尽管有这个缺点,它仍然可以在大多数击中时非常有效。

另外作为奖励,您可以在请求/响应周期之外使用它,例如芹菜任务或管理命令作业。