Кэш запросов в Django?

Я хотел бы реализовать декоратор, который обеспечивает кэширование для каждого метода, а не только представления. Вот пример использования.

У меня есть собственный тег, который определяет, если запись в длинном списке записей "любимый". Чтобы проверить, элемент является фаворитом, вам нужно запросить базы данных. В идеале вы бы выполните один запрос, чтобы получить все, а затем просто проверьте, что кешированный список против каждой записи.

Одним из решений является получение всех фаворитам в представлении, а затем передать которые устанавливаются в шаблон, а затем в каждый вызов тега.

В качестве альтернативы сам тег мог выполнить сам запрос, но только первый раз он назвал. Затем результаты могут быть кэшированы для последующих звонки. Положительным моментом является то, что вы можете использовать этот тег из любого шаблона, на любом просмотра, без предупреждения о представлении.

В существующем кэширующем механизме вы может просто кэшировать результат за 50 мс, и предположим, что это будет коррелировать с текущий запрос. Я хочу сделать это достоверность корреляции.

Вот пример тега, который у меня есть.

@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, не передавая его? Из тега я мог просто передать запрос, который всегда будет существовать. Но я хотел бы использовать этот декоратор из других функций.

Существует ли существующая реализация кэша для каждого запроса?

Ответы

Ответ 1

Используя настраиваемое промежуточное программное обеспечение, вы можете получить экземпляр кеша 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 = '[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()

Чтобы использовать промежуточное программное обеспечение, зарегистрируйте его в settings.py, например:

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

Затем вы можете использовать кеш следующим образом:

from myapp.request_cache import get_request_cache

cache = get_request_cache()

Обратитесь к api doc низкого уровня django для получения дополнительной информации:

API-интерфейс низкоуровневого кэша Django

Для использования кэша запросов должно быть легко изменить декоратор memoize. Посмотрите на библиотеку декораторов Python для хорошего примера декоратора memoize:

Библиотека декораторов питона

Ответ 2

Я придумал хак для кэширования вещей прямо в объект запроса (вместо использования стандартного кеша, который будет привязан к memcached, файлу, базе данных и т.д.).

# get the request object 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().

Георгий.

Ответ 3

Через несколько лет супер-хак для кэширования операторов SELECT внутри одного запроса Django. Вам нужно выполнить метод 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 Django на stand-in с именем execute_sql_cache. Этот метод смотрит на sql, который должен быть запущен, и если это оператор select, он сначала проверяет локальный поток. Только если он не найден в кеше, он перейдет к выполнению SQL. В любом другом типе sql-инструкции он сбрасывает кеш. Существует некоторая логика, чтобы не кэшировать большие результирующие наборы, что означает что-то более 100 записей. Это делается для того, чтобы сохранить оценку набора ленивых запросов Django.

Ответ 4

Основная проблема, которую не решает другое решение, заключается в том, что 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 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 cache.
        """
        cache = cls.get_cache()
        if cache:
            cache.clear()

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

EDIT 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 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 из контекста, в котором он обычно недоступен, вы можете использовать одну из различных реализаций так называемого "промежуточного программного обеспечения глобального запроса", который можно найти в Интернете.

Ответ 5

Вы всегда можете выполнить кэширование вручную.

    ...
    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)
    ...

Ответ 6

Этот использует питон python как кеш (не кеш django), и он мертв простым и легким.

  • Всякий раз, когда поток уничтожается, кеш будет слишком автоматически.
  • Не требует никакого промежуточного программного обеспечения, и контент не маринован и не отбрасывается при каждом доступе, что происходит быстрее.
  • Протестировано и работает с gevent monkeypatching.

То же самое может быть реализовано с помощью памяти threadlocal. Я не знаю о каких-либо недостатках этого подхода, не стесняйтесь добавлять их в комментарии.

from threading import currentThread
import weakref

_request_cache = weakref.WeakKeyDictionary()

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

Ответ 7

Ответ, данный @href_, замечательный.

На всякий случай вам нужно что-то более короткое, что также может сделать трюк:

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). Несмотря на этот недостаток, он по-прежнему может быть очень эффективным в большинстве просмотров.

Также в качестве бонуса вы можете использовать его за пределами циклов запроса/ответа, например, задачи сельдерея или задания команды управления.